In this lesson, you will learn how to evolve your library’s error types over time without breaking the compatibility of your public API.
As a project grows, the error variants you initially exposed often become insufficient. You might need to add new failure modes or provide richer context. If your library is consumed by others, you cannot simply change an enum signature because that is a breaking change. To evolve gracefully, we use the "Opaque Error Pattern" combined with "Non-Exhaustive" enums.
The Evolution Strategy
The primary rule in Rust for forward-compatible enums is the #[non_exhaustive] attribute. This prevents users from matching on your error type exhaustively, meaning you can add new variants later without causing compilation errors in downstream crates.
However, once you decide an error type needs a fundamental shift—perhaps from a simple enum to a complex struct-based tree—you can use an "Error Wrapper" to bridge the gap.
Consider this progression:
// Version 1: Simple enum
#[non_exhaustive]
pub enum DataError {
Io,
NotFound,
}
// Version 2: Expanding to a more descriptive structure
// We transition the old enum into a variant of a new, larger type
#[non_exhaustive]
pub enum DataError {
Network(NetworkError),
Storage(StorageError),
#[doc(hidden)]
__NonExhaustive,
}
Bridging the Gap
When you transition, keep the old error type as a fallback or map it internally to the new structure. By using the From trait to convert legacy error types into your new architecture, you ensure that older codebases calling your library still function correctly.
For internal logic, you keep your error hierarchy modular. If you need to add context, you evolve the struct members inside your variants rather than changing the enum signatures themselves. This keeps the "shape" of your error handling stable while the "content" becomes more informative.
Always treat your error type as a public interface that requires as much versioning discipline as your core logic. By shielding the internal structure with #[non_exhaustive] and using internal From conversions, you can refactor your entire failure hierarchy while keeping your external API users completely unaware of the internal upheaval.
Key Takeaway: Use #[non_exhaustive] on error types and internal From implementations to refactor your error hierarchy without breaking the semantic contract of your public API.