Mehdi Akiki
Rust Failure Atlas / Cargo and dependencies

RFA-098 · Case file with fixtures · Case 70 of 694 · Compiler evidence

E0425: When a Cargo Feature Removes the Function You Call

Conditional compilation removes inactive items before name resolution. Treat feature combinations as supported products: provide complete fallbacks, gate callers consistently, and test the intended matrix.

Reviewed
Rust
Rust 1.98.1
Targets
all targets; active cfg values differ
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
Conditional compilation deletes non-matching items before later name resolution, so a disabled feature means the function is absent rather than dormant.
First discriminating check
Inspect the active Cargo feature graph and add an explicit fallback or gate the caller under the same complete condition.

The function is visible in the editor, but absent from the compiled program:

#[cfg(feature = "fast")]
fn backend() -> &'static str {
    "fast"
}

fn main() {
    backend();
}

Without the fast configuration, Rust 1.98.1 reports E0425 and notes that the item was configured out. The failing fixture reproduces the mechanism directly.

A cfg attribute is not a runtime if. The inactive item is removed before later compiler stages resolve the call. From the compiled crate's perspective, backend does not exist.

Cargo features become cfg values

Cargo enables package features and passes corresponding cfg(feature = "name") values to that package. The Cargo features reference explains feature definitions and unification. The Rust Reference on conditional compilation defines cfg predicates and how attributes include or exclude code.

The compiler does not invent Cargo features. Running rustc directly with no --cfg feature=... produces the disabled branch, which is useful for isolating the language behavior. In a Cargo build, the manifest and resolved graph determine which values are active.

Provide a complete branch when the API always exists

If callers should always have a backend, define both configurations:

#[cfg(feature = "fast")]
fn backend() -> &'static str {
    "fast"
}

#[cfg(not(feature = "fast"))]
fn backend() -> &'static str {
    "portable"
}

The repaired fixture verifies the default fallback. Exactly one definition exists for either feature state.

This design keeps the public capability stable while changing its implementation. It is appropriate only when the fallback has honest semantics. Returning a placeholder or silently disabling validation can be worse than a compile error.

Gate the caller when the capability is optional

If no meaningful fallback exists, the caller belongs under the same capability boundary. This may mean conditionally exporting an API, conditionally building a binary, or returning a typed “unsupported” result from an always-present facade.

I avoid scattering the same cfg expression across many files. A small module with one selected implementation reduces the chance that declarations and calls use slightly different conditions.

Feature names belong to the package being compiled

A dependency feature is activated through Cargo syntax such as dependency/feature, but cfg(feature = "...") inside the current package refers to features declared by the current package. Code sometimes checks a dependency's feature name locally and remains permanently disabled.

The current package should expose its own feature and forward it to the dependency when that is part of its contract. Inspecting cargo tree -e features helps show who enabled what.

Features are unified, not selected per dependent

Within one normal dependency resolution, Cargo commonly builds a package with the union of features requested by its dependents. Features should therefore be additive. Two mutually exclusive modes expressed as independent features can both become active.

When combinations are invalid, I reject them with a focused compile_error! and explain the supported alternatives. Better still, I model an implementation choice through types, separate packages, or explicit configuration rather than conflicting additive features.

Test the matrix, not only the default

cargo test usually exercises default features. A trustworthy library also checks:

  • no default features,
  • all features,
  • each important individual feature,
  • supported combinations,
  • target-specific combinations where cfg values differ.

I keep the matrix intentional because the power set becomes impossible quickly. Unsupported combinations should be documented rather than left to fail randomly.

Editors can show code that rustc removes

Language servers often display inactive code dimly and may analyze multiple configurations approximately. The editor view is not evidence of the active build. I inspect the exact Cargo command, package selection, target, and feature flags from the failing environment.

Build scripts can emit custom cfg values too. Modern rustc can warn about unexpected cfg names, but the value still needs to be declared and tested consistently.

My first checks

When E0425 says an item was configured out, I do not start with imports. I record:

  1. The package containing the item.
  2. The cfg predicate on the item and every parent module.
  3. The active features in the resolved graph.
  4. The target cfg values.
  5. Whether the API needs a fallback or should be absent.

Then I reduce the condition to a complete truth table. The compiler error is useful because it exposes a partial product configuration. The durable repair makes every supported feature combination internally complete and continuously tested.