Error

"could not compile" — General Troubleshooting Steps

Start by running `cargo clean` to wipe the build cache, then execute `cargo build -vv` to see the full compiler output and pinpoint the exact error location.

When the build fails

You're deep in the flow. You just added a dependency, tweaked a struct, or refactored a module. You run cargo build. The terminal spits out a wall of red text ending with "could not compile". Your heart skips. Did you break the compiler? Did you break the project? Or is this just Rust doing its job?

The message "could not compile" isn't the error. It's the headline. The compiler found something it can't handle, but the standard output often hides the details to keep things tidy. You're looking at the summary, not the diagnosis. The real problem lives in the error code, the file path, and the suggestion that usually follows.

Treat "could not compile" as a signal to look closer, not a reason to panic. The compiler is trying to tell you exactly what went wrong. You just need to know where to look.

The anatomy of a compiler error

Rust errors follow a strict structure. Every error contains an error code, a message, a span pointing to the source location, and often a "help" line with a fix. The error code is the key. It lets you search for the exact issue and confirms you're looking at the right problem.

/// Demonstrates a type mismatch that triggers E0308.
fn main() {
    // The compiler expects an i32 but receives a &str.
    // This produces E0308: mismatched types.
    let count: i32 = "four";
    
    // The compiler rejects this with E0308.
    // It highlights the assignment and suggests a conversion.
    println!("Count: {}", count);
}

When you see E0308, you know immediately that a type doesn't match. The error message will show the expected type and the found type. The "help" line might suggest adding a .into() or changing the type annotation. Read the help line. The compiler often knows the fix before you do.

Some errors are more complex. E0277 means a trait bound isn't satisfied. E0432 means an import is unresolved. E0502 means you're borrowing something mutably while it's already borrowed immutably. Each code maps to a specific category of mistake. If you don't recognize the code, copy it and search. The Rust community has documented every error code.

Read the error code first. It tells you the category of the problem before you waste time guessing.

Verbose output and hidden details

Standard cargo build output truncates information to reduce noise. This is helpful for simple errors but dangerous when the problem is subtle. The -vv flag forces Cargo to print the full command line passed to rustc and the complete error message. This reveals details that are otherwise hidden.

# Run the build with verbose output to see the full compiler command.
# This shows which crate failed, which features were enabled,
# and the exact flags passed to rustc.
cargo build -vv

Verbose output is essential when a dependency fails to compile. The standard output might just say "could not compile openssl-sys". The verbose output shows the full error from the dependency's build script, including which system library is missing or which header file couldn't be found. It also shows the feature flags Cargo enabled. Sometimes a crate compiles fine with default features but fails when a specific feature is activated. The verbose output exposes this.

Developers use cargo check for fast iteration. It runs the compiler's analysis phase without generating binaries. It catches syntax and type errors much faster than cargo build. Use cargo check during development and cargo build when you need the executable.

Trust the verbose output. When the error is vague, verbosity is your friend. Run -vv and look for the command line.

Real-world failure modes

Most "could not compile" errors fall into a few categories. Missing dependencies, version conflicts, system library issues, and toolchain mismatches account for the vast majority of build failures.

Dependency resolution

Cargo manages dependencies through Cargo.toml and Cargo.lock. If a dependency version doesn't exist, conflicts with another dependency, or requires a newer Rust version, the build fails. The error usually points to the dependency resolution step.

[package]
name = "example"
version = "0.1.0"
edition = "2021"

[dependencies]
# If this version doesn't exist or conflicts, cargo fails.
# The error points to the dependency resolution step.
serde = { version = "99.0", features = ["derive"] }

When dependency resolution fails, check your Cargo.toml for typos. Verify that the version exists on crates.io. If you have multiple crates requiring different versions of the same dependency, Cargo tries to unify them. If it can't, you get a conflict error. Run cargo update to refresh the lockfile and resolve conflicts. If the conflict persists, you may need to adjust version ranges or use cargo tree to visualize the dependency graph.

System libraries

Many Rust crates bind to C libraries. These crates require the corresponding system library to be installed. If the library is missing, the build script fails. The error message usually mentions pkg-config or a missing header file.

For example, openssl-sys requires OpenSSL development headers. On Ubuntu, you need libssl-dev. On macOS, Homebrew's openssl package. On Windows, the vcpkg toolchain. The error message will explicitly state which library is missing. Install the system dependency and retry the build.

Check your system libraries. Rust bindings are just bridges; if the bridge is missing, the build falls.

Toolchain and MSRV

Every crate has a Minimum Supported Rust Version (MSRV). If your compiler is older than the MSRV, the build fails. The error might mention a missing feature, an unstable syntax, or a trait that doesn't exist yet.

Check the crate's documentation or Cargo.toml metadata for the MSRV. If you need a newer compiler, use rustup to install it.

# Install a specific Rust version and set it as the default.
# This ensures your compiler meets the crate's MSRV.
rustup install 1.75.0
rustup default 1.75.0

Developers prefer rustup override set in the project directory over changing the global default. This keeps your toolchain isolated per project. Run rustup override set 1.75.0 inside the project folder to pin the toolchain locally.

Build scripts

Build scripts (build.rs) run before the main compilation. They can generate code, configure environment variables, or check for system dependencies. If a build script fails, the build fails. The error might be buried in the output.

Verbose output helps here. Look for lines starting with Running target/debug/build/.... If the build script panics or exits with an error, the verbose output shows the panic message or the exit code. Fix the build script or install the missing system dependency.

Don't ignore build script errors. They often contain the root cause of the failure.

Macros and generated code

Macros generate code at compile time. When a macro fails, the error points to the generated code, not your source. This can be confusing. The error might reference a file that doesn't exist or show syntax that looks like gibberish.

/// Demonstrates a macro that generates invalid code.
macro_rules! bad_macro {
    () => {
        // This generates code with a syntax error.
        // The error points to the macro expansion, not the call site.
        let x = ;
    };
}

fn main() {
    // Invoking the macro triggers the error.
    bad_macro!();
}

When a macro error looks opaque, use cargo expand to see the generated code. This reveals what the macro produced and helps you locate the mistake. Install the tool with cargo install cargo-expand. Run cargo expand to print the expanded source.

Macros generate code you didn't write. If the error looks like gibberish, expand the macro.

Stale artifacts and cache issues

Cargo caches build artifacts to speed up subsequent builds. Sometimes the cache gets corrupted or contains stale data. This can cause phantom errors that disappear after a clean build.

# Remove all build artifacts and force a full rebuild.
# Use this when you suspect stale state is causing errors.
cargo clean
cargo build

cargo clean is nuclear. It deletes the target directory and forces Cargo to rebuild everything. Use it when you've updated dependencies, switched toolchains, or encountered errors that make no sense. It's slow, but it rules out cache issues.

Developers run cargo clean sparingly. It's a troubleshooting step, not a routine command. If you find yourself cleaning often, investigate the root cause.

Don't hoard artifacts. Clean when the state feels wrong.

Decision matrix

Pick the right tool for the symptom. Precision beats brute force.

Use cargo check when you want fast feedback on syntax and type errors without generating binaries. Use cargo build -vv when the standard error message is vague or you need to see the exact command line passed to the compiler. Use cargo clean when you suspect stale build artifacts are causing phantom errors after a dependency update or toolchain switch. Use rustup update when a crate requires a newer compiler version or you encounter a known compiler bug that has been fixed in a recent release. Use cargo update when dependency version conflicts prevent compilation and you need to resolve the lockfile. Use cargo expand when a macro error points to generated code and you need to inspect the expansion. Use rustup override set when you need to pin a specific toolchain for a project without affecting your global default.

Match the tool to the symptom. Precision beats brute force.

Where to go next