The dependency conflict
You add a new crate to your Cargo.toml. You run cargo build. Instead of a clean compile, the terminal explodes with a wall of red text about semver incompatibilities. One crate wants log 0.4, another wants log 0.3. Cargo gives up. You're stuck.
This is the dependency resolution conflict. It happens to everyone. Rust's package manager is strict by design. When two crates disagree on a shared requirement, Cargo needs help to pick a winner. The error message looks scary, but it contains all the information you need. You just have to read it and apply the right tool.
How Cargo resolves dependencies
Cargo is a constraint solver. It doesn't just download files. It treats every crate version as a variable and every requirement as a constraint. Its job is to find a set of versions where every constraint is satisfied. If no such set exists, the build fails.
Every dependency in Cargo.toml specifies a version range. Writing serde = "1.0" is shorthand for ^1.0. The caret means "compatible with 1.0". Cargo interprets this as >=1.0.0 and <2.0.0. It allows patch and minor updates but blocks major updates. This follows semantic versioning. Major versions can break APIs. Minor and patch versions should not.
When you have multiple dependencies, Cargo looks for the intersection of their ranges. Crate A might require common ^1.2. Crate B might require common ^1.5. The intersection is >=1.5.0 and <2.0.0. Cargo picks the highest version in that range, usually 1.9.0 or whatever is latest. If Crate C requires common ^2.0, the intersection with ^1.2 is empty. No version satisfies both. Resolution fails.
Minimal example
Here is the simplest case. Two direct dependencies pull in incompatible versions of a shared library.
// Cargo.toml
[dependencies]
// Alpha depends on common 1.x
alpha = "1.0"
// Beta depends on common 2.x
beta = "1.0"
When you run cargo build, Cargo checks the registry. It sees alpha requires common ^1.0. It sees beta requires common ^2.0. The ranges do not overlap. Cargo rejects the build with an error like error: failed to select a version for the requirement \common = "^1.0"``.
The error lists the requirements. It shows alpha wants ^1.0 and beta wants ^2.0. It proves no version exists that satisfies both. The conflict is structural. You cannot build this project until one of the requirements changes.
Read the error carefully. It tells you exactly which crates are fighting. That list is your starting point. Don't guess. Use the data Cargo gives you.
The lock file and updates
Most projects have a Cargo.lock file. This file records the exact version of every dependency, including transitive ones. It locks the dependency graph. When you run cargo build, Cargo checks the lock file. If the lock file satisfies all requirements, Cargo uses those versions. It skips resolution. This makes builds fast and reproducible.
Conflicts often appear when you add a new dependency. The new crate might require a version of a transitive dependency that is incompatible with what's locked. Cargo tries to update the lock file. If it can't find a compatible version, the build fails.
The first fix is usually cargo update. This command tells Cargo to re-resolve the graph and update the lock file. It fetches the latest compatible versions from the registry. Sometimes the conflict disappears because a transitive dependency has been updated to support a wider range.
# Update the lock file for all dependencies
cargo update
This works when the conflict is stale. Maybe common 1.9 was locked, but common 1.10 now supports the feature you need. cargo update bumps it.
The community convention is to avoid cargo update without arguments in large projects. Updating everything can pull in unexpected changes. Use cargo update -p crate-name to update a specific crate. This minimizes the blast radius.
# Update only the 'log' crate and its dependencies
cargo update -p log
Trust the lock file. It guarantees that your build works today. When you update, you are accepting new versions. Test after every update.
Realistic debugging
In real projects, conflicts are rarely between direct dependencies. They happen deep in the transitive graph. You have fifty crates. Two of them disagree on a tiny utility library. The error message lists the requirements, but it doesn't always show the full path.
Use cargo tree to visualize the graph. This command prints the dependency hierarchy. You can filter it to find the conflict.
# Show the dependency tree
cargo tree
# Show what depends on 'common'
cargo tree -i common
The -i flag shows reverse dependencies. It lists every crate that pulls in common. You can see the versions. If you see common 1.5.0 and common 2.0.0 in the output, you have a conflict. Cargo might have already failed, but cargo tree helps you understand the structure before you fix it.
Sometimes the conflict is about features. Cargo unifies features. If Crate A enables feature-x on common and Crate B enables feature-y, Cargo enables both. If common has mutually exclusive features, this causes a conflict. The error mentions features. Look for feature in the error text. You might need to disable a feature in your Cargo.toml.
[dependencies]
# Disable a problematic feature
common = { version = "1.0", default-features = false }
Convention aside: default-features = false is a common pattern when you want to avoid pulling in heavy or conflicting optional dependencies. Use it when you know exactly which features you need.
Strategies and decisions
When you hit a conflict, you have options. Pick the right one based on the situation.
Use cargo update when you want Cargo to re-resolve the entire dependency graph and pull in the latest compatible versions. This works when the conflict stems from outdated transitive dependencies that have since been updated to support newer semver ranges.
Use cargo update -p crate-name when you suspect a specific crate is holding back the resolution. This updates only that crate and its direct dependencies, keeping the rest of your lock file stable.
Use explicit version pinning in Cargo.toml when you need to force a specific version of a direct dependency to satisfy multiple downstream requirements. Write crate = "1.2.3" or crate = "^1.2" to narrow the range Cargo can choose from.
Use the [patch] section in Cargo.toml when you need to override a transitive dependency with a local development version or a fork. This is the standard way to test fixes for upstream crates without waiting for a release.
Use cargo tree to diagnose the conflict before applying fixes. This command prints the dependency graph and helps you identify which crates are pulling in incompatible versions.
The [patch] section is powerful. It lets you substitute a crate for the whole workspace.
# Cargo.toml
[patch.crates-io]
# Override 'common' with a local path
common = { path = "../my-common-fix" }
This replaces common everywhere. It's useful for debugging. It's also risky. If you patch a crate, you take responsibility for its compatibility. Use patches sparingly. Remove them when the upstream fix lands.
Pitfalls and errors
Pinning versions too tightly causes future conflicts. Writing crate = "=1.2.3" locks you to that exact version. If another crate requires 1.2.4, you break. Avoid exact pins unless you have a strong reason. Prefer ranges. Let Cargo do its job.
Ignoring the lock file causes non-reproducible builds. If you don't commit Cargo.lock, different developers might get different versions. This leads to "it works on my machine" bugs. For binary crates, always commit Cargo.lock. For library crates, omit it. Let downstream users resolve their own conflicts.
Another pitfall is fighting the compiler with unsafe workarounds. Dependency conflicts are not a code problem. You can't fix them with unsafe. You must fix the graph. If you can't resolve the conflict, you might need to fork a crate or find an alternative.
The error E0432 is a code error, not a dependency error. If you see E0432, the dependency resolved, but the code is wrong. Don't confuse resolution errors with compilation errors. Resolution happens first. If resolution fails, you never get to compilation.
Convention aside: cargo check is faster than cargo build. Use cargo check when debugging dependencies. It runs the resolver and type checks without linking. It gives you feedback quicker.