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
cargo new --lib menu
cd menu// menu/src/lib.rs
pub enum Menu {
Pizza,
Burger,
Tacos,
}
impl Menu {
pub fn price(&self) -> f32 {
match self {
Menu::Pizza => 100.0,
Menu::Burger => 180.0,
Menu::Tacos => 60.0,
}
}
} Creating an App That Uses the Library
cd ..
cargo new restaurant_app
cd restaurant_app# restaurant_app/Cargo.toml
[dependencies]
menu = { path = "../menu" }// restaurant_app/src/main.rs
use menu::Menu;
fn main() {
let order = Menu::Pizza;
match order {
Menu::Pizza => println!("You ordered pizza for ${}", order.price()),
Menu::Burger => println!("You ordered a burger for ${}", order.price()),
Menu::Tacos => println!("You ordered tacos for ${}", order.price()),
}
} The Breaking Change Problem
// menu/src/lib.rs
pub enum Menu {
Pizza,
Burger,
Tacos,
Burrito, // New item!
}
impl Menu {
pub fn price(&self) -> f32 {
match self {
Menu::Pizza => 100.0,
Menu::Burger => 180.0,
Menu::Tacos => 60.0,
Menu::Burrito => 150.0, // Add price for burrito
}
}
}error[E0004]: non-exhaustive patterns: `Menu::Burrito` not covered
--> src/main.rs:6:11
|
6 | match order {
| ^^^^^ pattern `Menu::Burrito` not covered The Solution: Using #[non_exhaustive]
// menu/src/lib.rs
#[non_exhaustive]
pub enum Menu {
Pizza,
Burger,
Tacos,
}
impl Menu {
pub fn price(&self) -> f32 {
match self {
Menu::Pizza => 100.0,
Menu::Burger => 180.0,
Menu::Tacos => 60.0,
}
}
}// restaurant_app/src/main.rs
use menu::Menu;
fn main() {
let order = Menu::Pizza;
match order {
Menu::Pizza => println!("You ordered pizza for ${}", order.price()),
Menu::Burger => println!("You ordered a burger for ${}", order.price()),
Menu::Tacos => println!("You ordered tacos for ${}", order.price()),
_ => println!("You ordered something for ${}", order.price()),
}
}_ not covered” Adding New Variants Without Breaking Changes
// menu/src/lib.rs
#[non_exhaustive]
pub enum Menu {
Pizza,
Burger,
Tacos,
Burrito, // New item added safely!
}
impl Menu {
pub fn price(&self) -> f32 {
match self {
Menu::Pizza => 100.0,
Menu::Burger => 180.0,
Menu::Tacos => 60.0,
Menu::Burrito => 150.0,
}
}
} 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
#[non_exhaustive]
pub struct MenuConfig {
pub theme: String,
pub show_prices: bool,
}
impl MenuConfig {
pub fn new(theme: String) -> Self {
Self {
theme,
show_prices: true,
}
}
}// This fails to compile! (in the app crate, not in the library)
let config = MenuConfig {
theme: "dark".to_string(),
show_prices: false,
};let config = MenuConfig::new("dark".to_string());println!("{}", config.theme); References