Avoiding Breaking Changes with Rust’s #[non_exhaustive]
In this post, I will explain how the #[non_exhaustive] attribute helps library authors avoid breaking changes in Rust. When you add new variants to public enums or new fields to public structs, you create a breaking change. This means any crates that depend on your library will need to update their code before they can use your new version. Rust’s Official doc explanation: The non_exhaustive attribute indicates that a type or variant may have more fields or variants added in the future. Think of it as a “future-proofing” attribute that says “hey, I might add more to this later, so don’t assume this is complete” When you mark an enum with #[non_exhaustive], external crates must include a wildcard pattern (_) in their match statements, so adding new variants won’t break their code. For structs, it prevents direct construction using struct literal syntax and requires wildcards (..) in pattern matching, allowing you to safely add new fields later. This way, you can evolve your API without forcing breaking changes on your users - they write defensive code once, and your library can grow without breaking their applications. However, these restrictions only apply to downstream crates that depend on your library. Within the defining crate itself, these rules don’t apply. I’ll start with an enum example since we used this attribute for the ErrorKind enum in the RTC HAL chapter of the Rust Embedded Drivers (RED) book. That’s what made me want to write this post. Instead of explaining it in detail in the book, I decided to cover it separately here. Let’s say we’re creating a library called menu with an enum called First, create a new library: In src/lib.rs, define a simple menu enum: Now create an app that uses your menu library: Add your local menu library to Cargo.toml: Use the library in the app: When you run the app, you should get output like “You ordered pizza for $12.99”. Everything works great! A month later, you want to add burritos to your menu. Update the library: Now try to run the restaurant app, you will get the following compile error: The app is broken now! Anyone using your library will have to update their code before they can use your new version. This is a breaking change. According to the Cargo docs, this should be a major change. So as a library developer, you should bump your library version to the next major version (for example: 1.0.0 to 2.0.0) to maintain SemVer compatibility. Go back to your menu library and add the #[non_exhaustive] attribute: NOTE: adding non_exhaustive attribute itself is a breaking change because without the wildcard pattern, it will give compilation error. Now you have to update the restaurant app to handle the non-exhaustive enum: The _ wildcard is now required. Without it, you get this error: “non-exhaustive patterns: Now you can safely add the burrito: With this change, you can add more variants. No breaking change, no need to update the major version number. When you mark a struct with #[non_exhaustive], it prevents other crates from creating instances of that struct directly using the Inside your own crate, you can still create and use the struct normally. But external crates must use constructor functions you provide and include “..” when pattern matching. This way, you can safely add new fields to your struct in future versions without breaking anyone’s code. Let’s add a configuration struct to our menu library: We’ve marked MenuConfig with the #[non_exhaustive] attribute. If someone tries to create an instance of MenuConfig using struct literal syntax in the restaurant app, they’ll get the error “cannot create non-exhaustive struct using struct expression”: Instead, external crates must use the constructor function you provide: You can still access the fields since we marked them as pub: This way, you can safely add new fields like “font_size” or “language” to MenuConfig in future versions without breaking anyone’s code.#[non_exhaustive]
attribute solves this problem by telling the compiler that a type might gain new variants or fields in future versions, forcing external crates to write future-proof code from the start. Example: Marking enum with non_exhaustive attribute
Menu
that other developers can use in their apps. Let’s see what happens when you try to add a new variant to the enum. Creating the Menu Library
// menu/src/lib.rs
Creating an App That Uses the Library
# restaurant_app/Cargo.toml
[]
= { = "../menu" }
// restaurant_app/src/main.rs
use Menu;
The Breaking Change Problem
// menu/src/lib.rs
|
|
The Solution: Using #[non_exhaustive]
// menu/src/lib.rs
// restaurant_app/src/main.rs
use Menu;
_
not covered” Adding New Variants Without Breaking Changes
// menu/src/lib.rs
Example: Marking struct with non_exhaustive attribute
MyStruct { field: value }
syntax. They also can’t pattern match on it without using “..” to indicate there might be more fields in the future.// menu/src/lib.rs
// This fails to compile! (in the app crate, not in the library)
let config = MenuConfig ;
let config = new;
println!;
References