#[non_exhaustive]
#[non_exhaustive] is a semver-evolution attribute applied to a pub struct, pub enum, or individual enum variant that informs downstream crates that more fields or variants may be added in future minor releases. Within the defining crate itself, #[non_exhaustive] has zero restrictions—local code can still construct and match the type exhaustively.
Three Placement Positions of #[non_exhaustive]
| Placement | Effect on Downstream Crates (Outside Defining Crate) | Effect Inside Defining Crate |
|---|---|---|
1. On an enum (#[non_exhaustive] pub enum Error) |
Downstream match statements must include a wildcard arm _ => ... (E0004) |
Exhaustive match without _ still works! |
2. On a struct (#[non_exhaustive] pub struct Config) |
Downstream crates cannot construct Config { a: 1 } with struct literal syntax (E0639) and must use .. when destructuring let Config { a, .. } = cfg; |
Literal construction and full destructuring still work! |
3. On an enum variant (pub enum Event { #[non_exhaustive] Click { x: i32 } }) |
Downstream crates cannot construct Event::Click { x: 1 } directly and must match it with Event::Click { x, .. } |
Full construction and matching still work! |
Semver Problem Without `#[non_exhaustive]`:
v1.0.0: `pub enum Error { Io, Parse }`
Downstream writes: `match err { Error::Io => ..., Error::Parse => ... }`
v1.1.0: Library adds `Timeout` variant -> Breaks downstream's exhaustive `match`!
With `#[non_exhaustive] pub enum Error`:
v1.0.0: Downstream is required to write:
`match err { Error::Io => ..., Error::Parse => ..., _ => ... }`
v1.1.0: Adding `Timeout` variant is 100% backwards-compatible!
Constructing #[non_exhaustive] Structs via Builder or Default + Functional Update Note
Because downstream crates cannot write a struct literal Config { host: ..., port: ... } when Config is marked #[non_exhaustive] (even when all fields are pub!), always provide a constructor (Config::new(...)) or Default::default() alongside pub fields so downstream users can construct and mutate fields cleanly:
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct ClientOptions {
pub timeout_ms: u64,
pub max_retries: u8,
}
impl ClientOptions {
pub const fn new() -> Self {
Self {
timeout_ms: 5000,
max_retries: 3,
}
}
}
impl Default for ClientOptions {
fn default() -> Self {
Self::new()
}
}
#[non_exhaustive]
#[derive(Debug)]
pub enum ApiError {
RateLimited { retry_after_secs: u32 },
#[non_exhaustive]
ServerError {
status: u16,
},
}
fn main() {
// External callers construct via `ClientOptions::new()` and mutate `pub` fields:
let mut opts = ClientOptions::new();
opts.timeout_ms = 2500;
// External callers destructure using `..` to stay resilient to future fields:
let ClientOptions { timeout_ms, .. } = opts;
println!("configured timeout_ms = {}", timeout_ms);
let err = ApiError::ServerError { status: 503 };
match err {
ApiError::ServerError { status, .. } => println!("server error: {}", status),
_ => println!("other api error"),
}
}
configured timeout_ms = 2500
server error: 503
Note that on a #[non_exhaustive] struct, functional struct update syntax (ClientOptions { timeout_ms: 1000, ..ClientOptions::default() }) is also forbidden outside the defining crate because private or future fields could be added; mutating opts.timeout_ms = 1000; after let mut opts = ClientOptions::default(); is the supported pattern.