Attributes and Conditional Compilation

#[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.