RFA-103 · Case file with fixtures · Case 75 of 694 · Cargo workspace evidence
Why an Optional Cargo Dependency Is Missing From Rust Code
An optional dependency is absent from the crate graph until a feature activates it. Gate dependency, code, tests, and public API under one complete feature contract, including a valid feature-off build.
- Reviewed
- Rust
- Rust 1.98.1, Cargo 1.98.1
- Targets
- all targets
- Profiles
- check, dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- No active Cargo feature includes the optional dependency, so Cargo omits it from the graph and rustc receives no extern crate mapping.
- First discriminating check
- Find the feature that activates the optional dependency and verify that all code mentioning it uses the same complete condition.
An optional dependency still appears in Cargo.toml, so it is easy to believe Rust should at least know its name. It does not work this way. If no active feature enables the optional dependency, Cargo leaves it out of the dependency graph and rustc receives no --extern entry for it.
The failing manifest declares a local helper dependency with optional = true. The program imports helper::message() unconditionally. Cargo 1.98.1 reaches rustc without that crate and the compilation fails with E0433.
Optional means conditional graph membership
The Cargo optional dependencies documentation explains that an optional dependency is tied to a feature. In current explicit syntax, a feature can activate it with dep:helper.
This is not late loading. Cargo does not compile the dependency and wait for the program to use it. Feature resolution happens first, then conditional compilation shapes the crate, and only the selected graph is compiled.
I use this mental sequence:
feature requests -> resolved features -> dependency graph -> cfg-expanded Rust -> type checking
An error in the last stage may have its cause in the first one.
Gate the whole capability
The repaired manifest creates a named cli feature that activates dep:helper. The Rust file has a feature-on main and a feature-off main. Both configurations describe complete programs.
For a library, the boundary is larger. I check:
- imports and modules;
- public functions mentioning dependency types;
- trait implementations;
- tests, examples, and benchmarks;
- documentation examples;
- build-script assumptions.
Gating only the use statement is not enough when an ungated public signature still mentions the missing crate. The conditional compilation reference describes how cfg includes or excludes source constructs. I place the condition at the level of the capability, often on a module or impl block, instead of scattering tiny conditions across individual expressions.
Give the feature a product meaning
Cargo can create an implicit feature with the same name as an optional dependency. The dep: syntax suppresses that implicit public feature and lets me expose a clearer name such as postgres, compression, or cli.
This matters because features are part of a crate's user-facing configuration. An internal package name can change while the capability remains. One capability can also activate several optional dependencies and cfg values together.
I avoid a feature named use-helper. It tells consumers about implementation, not behavior.
Test both sides deliberately
A default build can hide the failure if the feature is enabled by default. I add at least these checks where optional behavior matters:
cargo check --no-default-features
cargo test --features cli
cargo check --all-features
For several independent features, pairwise or curated combinations may be needed. --all-features proves the maximal union builds, but it does not prove every meaningful minimal build works.
Targets can add another condition. A dependency may exist only under cfg(unix) and a feature. The Rust module must use the same logical contract. I prefer one named feature plus a clear target condition rather than repeating slightly different Boolean expressions.
Do not repair it by making everything mandatory
Removing optional = true makes the example compile, but it can be the wrong product decision. Optional dependencies reduce compile time, binary surface, native prerequisites, or functionality for consumers who do not need a capability.
Before making it mandatory, I ask why it was optional. If every useful build needs it, mandatory is honest. If only one adapter needs it, restoring the feature boundary is better.
The opposite shortcut is adding #[allow(unused_imports)]. This has no effect because the crate is unresolved, not unused.
My debugging sequence
When an apparently declared crate is unresolved, I check:
- The dependency kind and target table in
Cargo.toml. - Whether it is optional and which feature activates it.
- Active features with
cargo tree -e features. - The exact
cfgvalues usingcargo rustc -- --print cfgwhen needed. - Whether the feature-off build has a complete alternative path.
- Public API, tests, examples, and docs for ungated references.
The error message points at an import, but the useful repair is a configuration contract. Once the capability has one name and both enabled and disabled states are tested, the optional dependency stops behaving like a crate that randomly disappears.