An exhaustive match makes the compiler check that you handled every enum variant. That is useful when you want a new variant to trigger a review of existing code. A public library sometimes needs a different contract: callers should handle selected cases and provide a fallback for future ones.
#[non_exhaustive] expresses that choice at a crate boundary. It can mark an
enum, a struct or an individual enum variant. Its restrictions apply to code
outside the crate that defines the marked item.
See the enum rule with two crates
Save this library as errors.rs:
#[non_exhaustive]
#[derive(Debug)]
pub enum ConnectError {
Timeout,
Refused,
}
pub fn local_label(error: &ConnectError) -> &'static str {
match error {
ConnectError::Timeout => "timeout",
ConnectError::Refused => "refused",
}
}
local_label is in the defining crate, so its match can list all current
variants without a fallback. Now save a separate consumer as main.rs:
use errors::ConnectError;
fn label(error: &ConnectError) -> &'static str {
match error {
ConnectError::Timeout => "timeout",
ConnectError::Refused => "refused",
_ => "another connection error",
}
}
fn main() {
assert_eq!(label(&ConnectError::Timeout), "timeout");
assert_eq!(label(&ConnectError::Refused), "refused");
}
Build the library, then its consumer:
rustc --edition=2024 --crate-type=lib --crate-name errors errors.rs
rustc --edition=2024 main.rs --extern errors=liberrors.rlib
Run the resulting executable. Then remove the _ arm from the consumer and
compile it again. It fails with E0004 even though you listed both current
variants. The library's own match still compiles without that arm.
A nested mod would not create this distinction: modules in one crate share
the defining crate's access to the marked type.
What compatibility does this buy?
Without the attribute, adding a public enum variant can break downstream exhaustive matches. With the attribute already present, callers must account for additional variants from the start. Adding another ordinary variant can then preserve those matches without a breaking release.
That is not a promise that every possible addition is compatible. A variant
carrying a new field type can affect traits such as Send or Sync, and a
new error case can require different application behavior. Review the whole
API change. Adding #[non_exhaustive] to an existing exhaustive public enum
is itself breaking for callers that relied on exhaustive matching.
Cargo's SemVer guidance explains the relevant compatibility categories. Build compatibility and useful handling of a new case are separate questions.
Check your understanding What should a fallback arm do? Show answer Hide answer
Choose behavior that remains reasonable for an unfamiliar case: return a
general error, propagate the original error, or record it for diagnosis.
A binding such as other => report(other) preserves access to the value.
_ => todo!() compiles, but panics if that arm runs.
Do not assume every unknown connection error should be retried or ignored. The right response depends on the operation and the caller's policy.
You can observe this with the standard library
std::io::ErrorKind is defined in another crate and is non-exhaustive. This
complete one-file program handles one case and falls back for the others:
use std::io::ErrorKind;
fn message(kind: ErrorKind) -> &'static str {
match kind {
ErrorKind::NotFound => "The requested item was not found.",
_ => "An I/O operation failed.",
}
}
fn main() {
assert_eq!(message(ErrorKind::NotFound), "The requested item was not found.");
assert_eq!(message(ErrorKind::PermissionDenied), "An I/O operation failed.");
}
Even listing every currently documented ErrorKind variant would not remove the need for a fallback. This lesson uses an article so you can compare the library and consumer sides explicitly, rather than merely add a wildcard to an existing standard-library match.
Structs and individual variants
For a non-exhaustive struct defined in another crate, callers cannot use a
struct literal to construct it. For a named-field struct pattern, they must
include .. to allow additional fields. The library should provide a supported
way to obtain a value, such as a constructor, a builder or a Default
implementation. It can still construct the struct normally inside its own crate.
Marking an enum and marking one of its variants are different decisions. An enum-level attribute leaves room for new variants. Marking a named-field variant leaves room for more fields in that variant and restricts downstream construction and matching. Enum-level non-exhaustiveness alone does not prevent callers from constructing existing public variants, as the consumer example shows.
Tuple and unit forms have additional constructor and pattern restrictions; consult the Rust Reference when designing those APIs.
Choose the attribute for a reason
Use it when extending a public type is part of your compatibility plan and callers can handle unknown cases meaningfully. Error enums and configuration structs are common candidates, but they are not automatically good choices.
Leave an enum exhaustive when handling every variant is a useful obligation. For types used only inside their defining crate, this attribute does not relax local exhaustiveness checks. An application can contain several crates, so decide based on the actual boundary, not whether the project is called a library.
Older APIs sometimes used a hidden placeholder variant or a private dummy field to restrict callers. Those are real parts of the type: a public hidden variant can still be constructed, and private fields affect construction and patterns. The attribute states the extension policy directly without adding that placeholder data. Migrating an existing API still needs compatibility review.