Skip to content

← Shape Your Data: Structs, Enums, Pattern Matching step 8 of 26

Medium Primitives

The wildcard trap: `_` arms freeze your enum against the future

A supervisor turns signal lines into status strings.

pub fn handle(events: Vec<String>) -> Vec<String>
line status
start running
stop halted
pause paused
resume running
reconnect 3 retrying #3
anything else unknown

Read the starter before you read anything else

The starter is what this code looked like one release ago, when Signal had three variants and _ => "paused" was a perfectly reasonable way to say “the other one”. Since then someone added Resume and Reconnect.

Notice what did not happen. The build did not break. No test in the original suite failed. The wildcard quietly absorbed both new variants and started reporting a connection retry as paused. That is the entire lesson, and it is worth sitting with for a moment: the wildcard is the switch that turns exhaustiveness checking off. Everything the previous item promised — the compiler walking you to every site that needs updating — stops applying the moment you write _.

Two gates, deliberately

This problem also injects a lint that is normally off:

#![warn(clippy::wildcard_enum_match_arm)]

wildcard_enum_match_arm lives in clippy’s restriction group, which is allow-by-default and which you should never enable wholesale — its lints contradict each other on purpose. Cherry-picked, though, it is exactly the tool for this job, and under -D warnings a warn-level lint is a build failure. So the starter fails twice: the tests catch the wrong answers, and the lint catches the construct that caused them.

Keep the attribute in your submission. It is part of the exercise.

When a wildcard is right

This is a habit, not a commandment, and the honest version of it matters:

  • #[non_exhaustive] upstream enums require one. std::io::ErrorKind can gain variants in any release, so a match on it that compiles today without a wildcard would break your build on a routine toolchain upgrade. Rust makes you write _ there, and it is right to.
  • Genuinely large enums where thirty variants share one behaviour are more readable with a wildcard than with thirty copies of the same arm.
  • Everywhere else, the wildcard is buying you thirty seconds now and costing you a silent bug later.

There is also a middle ground most people never learn:

Signal::Reconnect { .. } => ...,   // ignores the payload
Signal::Reconnect(..)   => ...,    // same, for a tuple variant

These ignore the data but still name the variant, so adding a new variant still breaks the match. If your reason for reaching for _ was “I do not care about the fields”, this is what you actually wanted.

The rule is “know why you wrote it”, not “never write it”.

Related lints worth recognising

match_wildcard_for_single_variants — you wrote _ and it covers exactly one variant; just name it. match_wild_err_armErr(_) => panic!(...) throws away the error someone went to the trouble of handing you. single_match_else — a two-arm match that an if let ... else would say more plainly. All three are allow-by-default; they are worth knowing about even when the gate is silent.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…