In this lesson, you will learn how to design error enums so that adding new error variants in the future won’t break downstream code that relies on exhaustive pattern matching.
The Problem: Exhaustiveness
In Rust, match statements must be exhaustive. If you expose a public error enum, your users likely use match to handle every variant:
pub enum NetworkError {
Timeout,
ConnectionReset,
}
// User code
match error {
NetworkError::Timeout => { /* ... */ }
NetworkError::ConnectionReset => { /* ... */ }
}
If you later add NetworkError::AccessDenied to your library, every user's match statement will break because they aren't handling the new variant. This creates a breaking change for your API.
The Solution: The #[non_exhaustive] Attribute
To prevent this, you can mark your public error enum with the #[non_exhaustive] attribute. This attribute tells the Rust compiler that you intend to add more variants in the future.
#[non_exhaustive]
pub enum NetworkError {
Timeout,
ConnectionReset,
}
Once you apply this, two things change:
- Downstream consumers cannot exhaustively match. They are forced to include a wildcard (
_) arm in theirmatchblocks. - Library stability. You can now add
AccessDeniedto the enum without causing compilation errors for your users.
Handling the Wildcard
Because of the _ requirement, your users are now forced to handle unexpected future variants safely:
match error {
NetworkError::Timeout => { /* ... */ }
NetworkError::ConnectionReset => { /* ... */ }
_ => { /* Log unknown error or treat as generic failure */ }
}
By using #[non_exhaustive], you decouple the evolution of your library's error API from the downstream codebase's build stability. This is standard practice for public-facing crates where breaking changes must be kept to a minimum.
Key Takeaway: Apply the #[non_exhaustive] attribute to public error enums to reserve the right to add new error variants without breaking your users' exhaustive pattern matching logic.