Skip to content

S16: Wildcard _ => match arm that hides new variants

S16Severity: Medium

The compiler cannot tell you when a new variant is not handled.

Clippy: wildcard_enum_match_arm

An enum that got a new variant, Refunded:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaymentStatus {
Pending,
Paid,
Failed,
Refunded, // added later
}

And a match that was written before it:

pub fn customer_message(status: PaymentStatus) -> &'static str {
match status {
PaymentStatus::Paid => "Thank you for your payment.",
PaymentStatus::Failed => "Your payment failed. Please try again.",
_ => "We are waiting for your payment.", // now also used for `Refunded`
}
}

When Refunded was added, this code still compiled. A refunded customer now reads “We are waiting for your payment.” The test worse_tells_refunded_customers_to_pay proves it.

  • It is the Java default: habit. A Java switch over an enum often ends with default, and before Java 21 the compiler did not check that every constant was handled.
  • It is shorter. When two variants have the same result, _ => saves a line.
  • It removes error E0004. When a match is not exhaustive, adding _ => ... is the quickest way to make it compile.
  • It switches off the check that makes enums valuable. Without a wildcard, adding a variant makes the compiler list every match that must handle it. With a wildcard, the compiler says nothing.
  • The bug is in code that nobody changed. The pull request that adds Refunded changes the enum. The wrong message comes from a match in another file, which is not in the diff. A reviewer has no reason to look there.
  • The default is often wrong for the new case. The wildcard was written for the variants that existed then. Nobody decided what it means for variants that did not exist yet.
  1. Worse_ => covers Pending, and silently also Refunded.

    pub fn customer_message(status: PaymentStatus) -> &'static str {
    match status {
    PaymentStatus::Paid => "Thank you for your payment.",
    PaymentStatus::Failed => "Your payment failed. Please try again.",
    _ => "We are waiting for your payment.", // now also used for `Refunded`
    }
    }
  2. BetterEvery variant is listed. The next new variant is a compile error here.

    pub fn customer_message(status: PaymentStatus) -> &'static str {
    match status {
    PaymentStatus::Pending => "We are waiting for your payment.",
    PaymentStatus::Paid => "Thank you for your payment.",
    PaymentStatus::Failed => "Your payment failed. Please try again.",
    PaymentStatus::Refunded => "Your payment was refunded.",
    }
    }

If two variants have the same result, list both with |: PaymentStatus::Pending | PaymentStatus::Failed => .... That keeps the check and the short code.

  • Types with no fixed list of variants: integers, strings, and tuples of them. A catch-all is required:
/// Integers and strings have no list of variants, so a catch-all is required.
pub fn http_reason(code: u16) -> &'static str {
match code {
200 => "OK",
404 => "Not Found",
500..=599 => "Server Error",
_ => "Other",
}
}
  • Enums from another crate marked #[non_exhaustive]. The compiler requires a wildcard, because the crate can add variants in a minor version. std::io::ErrorKind is an example.
  • A deliberate “every other case”, where the rule really holds for all future variants: “only an authorized payment can be captured, every other state is rejected”. See the state machine on Enums and pattern matching. A comment helps the reader.
  • matches!(x, Variant) and if let, which are about one variant by design.
  • Clippy wildcard_enum_match_arm (restriction group, off by default) reports a _ arm in a match on an enum. It confirms the example above. Enable it with #![warn(clippy::wildcard_enum_match_arm)] in a crate that has many domain enums.
  • Clippy match_wildcard_for_single_variants (pedantic) reports a _ that covers exactly one variant.
  • Search for _ => and check the type of each matched value. Integer and string matches are fine. Matches on your own enums need a reason.
In every match on our own enums, replace the _ => arm with the variants
it covers, listed by name (use | to combine variants with the same result).
Keep _ only for integers, strings, and #[non_exhaustive] enums from other
crates. Then enable clippy::wildcard_enum_match_arm for this crate and fix
what it reports.

Related: S15: bool flags or string constants instead of an enum, and Enums and pattern matching.