In this lesson, you will learn how to hide internal implementation details of your error types to maintain a stable API while still providing enough context for callers to make informed decisions.
Why Opaque?
In Rust, we often define errors as enums. While convenient, an enum exposes every possible failure variant to the public. If you add a new error case later, you break the code of any user performing an exhaustive match on your error. Opaque error types solve this by using a struct that hides its internal state, exposing only what the caller needs to know via methods.
Implementing Opaque Errors
Instead of forcing users to pattern match on an internal enum, you encapsulate the error inside a private struct and provide a public API for inspection:
pub struct ServiceError {
inner: ErrorKind, // Private enum
}
impl ServiceError {
pub fn is_transient(&self) -> bool {
matches!(self.inner, ErrorKind::NetworkTimeout | ErrorKind::Busy)
}
pub fn code(&self) -> u16 {
self.inner.to_http_code()
}
}
By keeping inner private to your module or crate, you can refactor your internal error logic—adding or removing variants—without changing the public surface area. The caller doesn't need to know why the service failed; they only need to know if it is a transient error they should retry, or a permanent error they should log and discard.
Balancing Transparency and Encapsulation
The goal isn't to hide information entirely, but to design a "contract" with your user. By providing semantic methods like is_transient() or retry_after(), you decouple the error’s internal representation from the consumer's logic. This makes your crate's API significantly more resilient to future changes, as you are free to expand your internal error variants without forcing consumers to update their match statements across their entire codebase.
Opaque types allow you to evolve your error-handling logic internally without breaking the guarantee you’ve provided to your users.
Key Takeaway: Design opaque error types by exposing semantic methods instead of internal variants to preserve long-term API stability and encapsulate implementation details.