Rust Cheatsheet - Rust & Cargo Command Reference
This reference is for Rust developers — whether you are still wrestling with the borrow checker or shipping a production crate — and centers on Cargo, the toolchain you touch every day. It covers scaffolding projects, building (debug vs release), running binaries, testing, generating docs, running Clippy lints, formatting with rustfmt, and cross-compiling for a target. Unlike a flat list of cargo subcommands, entries follow the workflow you actually repeat: edit → check/build → test → lint → fmt → release. After reading, you should be able to take a fresh crate from cargo new through a clean Clippy + test pass to a release build.
Typical Use Case
For developers writing services, CLIs or concurrent workers in Rust. The learning curve concentrates in ownership and borrowing, so this sheet separates the two halves: cargo on one side (project layout, dependencies, testing, checking, publishing, cross-compilation) and language core on the other (ownership, borrowing, lifetime elision, Result and ? propagation). The habit worth changing first is cargo check instead of cargo build during development — it is dramatically faster for type and borrow verification.
Project Management 9
cargo new project_namecargo new --lib project_namecargo initcargo buildcargo build --releasecargo runcargo run --releasecargo checkcargo cleanDependency Management 6
cargo add serdecargo add serde --features derivecargo remove serdecargo updatecargo treecargo tree --invertTesting & Docs 5
cargo testcargo test -- --nocapturecargo test test_namecargo doc --opencargo doc --no-depsCode Quality 4
cargo clippycargo clippy --fixcargo fmtcargo fmt -- --checkPublish & Cross-Compile 6
cargo publishcargo publish --dry-runcargo build --target x86_64-unknown-linux-muslrustup target listrustup target add x86_64-pc-windows-gnucargo install cargo-editCore Syntax 14
let s2 = s1;let r = &s;let r = &mut s;fn foo<'a>(x: &'a str) -> &'a strfn foo(x: &str) -> &strmatch x { Some(v) => v, None => 0 }if let Some(v) = opt {}while let Some(v) = iter.next() {}let Ok(v) = res else { return; };Result<T, E>Option<T>fn foo() -> Result<T, E>result?#[derive(Debug, Clone, PartialEq)]Module System 7
use std::collections::HashMap;use std::io::{self, Read};mod my_module;pub fn foo() {}pub(crate) fn internal()pub use crate::utils::helper;crate::module::func()Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
cargo new --lib | Creates a library crate whose entry point is src/lib.rs | cargo new --lib mylib |
cargo check | Type-checks without producing a binary, which makes it the fastest gate of all | cargo check |
cargo build --release | Optimised build, with the artefacts landing in target/release | cargo build --release |
cargo add serde --features derive | Adds a dependency and switches on one of its features | cargo add serde --features derive |
cargo update | Upgrades within the constraints declared in the lock file | cargo update |
cargo tree --invert | Reverse lookup: which crates end up depending on this package | cargo tree --invert serde |
cargo clippy --fix | Runs the linter and applies its automatic fixes | cargo clippy --fix |
cargo fmt -- --check | A formatting check suitable as a CI gate | cargo fmt -- --check |
result? | Error propagation: return early the moment something fails | let v = parse(s)?; |
&s / &mut s | Immutable and mutable borrowing | fn f(s: &mut String) |
#[derive(Debug, Clone)] | Auto-generates the trait implementations | #[derive(Debug, Clone, PartialEq)] |
rustup target add <triple> | Registers a target triple so cross-compilation can proceed | rustup target add x86_64-pc-windows-gnu |
Common pitfalls
SymptomThe compiler reports cannot move out of ... because the value is used again afterwards.
CauseAssignment and argument passing move ownership by default, which invalidates the original binding.
FixPass by reference with &s instead; clone deliberately only when you genuinely need two copies.
SymptomTwo simultaneous mutable borrows in one scope trigger a borrow conflict.
CauseRust permits only one &mut borrow to be alive at any moment.
FixSplit the borrows into separate scopes, or reshape the data so their lifetimes do not overlap.
SymptomA function returning a reference fails with missing lifetime specifier.
CauseThe return value borrows from an argument and the compiler cannot infer how the two lifetimes relate.
FixAnnotate it explicitly, fn foo<'a>(x: &'a str) -> &'a str, or take and return String so no reference is involved.
Symptomcargo build suddenly gets slow or reports odd compile errors that vanish on a rebuild.
CauseThe incremental cache inside target has drifted out of sync with the current source state.
FixRun cargo clean to empty target and rebuild from scratch.
SymptomThe build breaks after a dependency upgrade, and the lock file changed on its own.
Causecargo update also bumps the transitive dependencies recorded in Cargo.lock.
FixBack up the lock file first, then pin a single crate with cargo update -p <crate> --precise <ver>.
SymptomCross-compilation fails for want of a linker or an unknown target.
CauseThe target triple is not installed, or the cross toolchain for that platform is missing.
FixInstall it with rustup target add <triple> and confirm the required cross linker is in place.
Troubleshooting
1Run the fastest check first
cargo checkIt produces no binary, so types and borrows are verified in the least time; it is the default while iterating on code.
2Find out who pulled in a dependency
cargo tree --invert serdeThe reverse query shows the blast radius of a transitive bump and whether the crate can go.
3Rebuild from scratch when the cache goes bad
cargo clean && cargo buildIt empties the target directory, so use it only when the cache looks inconsistent, because everything recompiles.
4Dry-run the publish step
cargo publish --dry-runNothing is uploaded; it just validates the packaging and metadata against the crates.io rules.
5The quality gate for CI
cargo fmt -- --check && cargo clippyFormatting and lint ahead of the build is the smallest useful combination for continuous integration.
Command Examples
Add a dependency with its feature instead of editing the manifest by hand
cargo add serde --features deriveThe derive feature is what makes #[derive(Serialize)] usable. cargo add also picks a version compatible with your current toolchain and writes it into Cargo.toml, whereas hand-editing tends to leave behind a constraint that will not resolve.
Output
Adding serde v1.0.210 to dependencies
Adding serde_derive v1.0.210 to dependenciesSee who pulls in a package before upgrading it
cargo tree --invert serdeThe inverted tree answers "who depends on me", while plain cargo tree answers "what do I depend on". Before a change that can cascade — adding a feature, say — run --invert to size the blast radius and then decide whether to proceed.
Output
my-app v0.1.0 └── serde v1.0.210
Make println output visible while debugging
cargo test -- --nocaptureThe harness captures stdout by default, so passing tests print nothing — which is the usual reason "my println never showed up". -- --nocapture forwards the flag to the test binary and disables capture. The double -- is required: it separates cargo arguments from the ones handed to the test runner.
Gate formatting in CI with fmt --check
cargo fmt -- --checkWithout -- --check, cargo fmt rewrites files in place. With it, the command only checks and exits non-zero on a diff. Use the check form in CI and plain cargo fmt locally to actually write the formatting.
Output
Diff in /src/main.rs at line 14: - let x=1; + let x = 1;
let-else for early return in one statement
let Ok(value) = config.get("port") else {
return;
};match / if let / while let / let-else are four spellings of the same idea. let-else requires the else branch to diverge — return, break, panic — so the compiler can narrow the binding for later use instead of leaving you unwrapping Options layer by layer.
Common Pitfalls
- cargo clean deletes the entire target directory and every incremental cache with it — minutes of compilation gone. Use it only when incremental results are genuinely corrupt.
- Commit Cargo.lock, including for library crates: an uncommitted lock file lets every machine resolve different transitive versions, and the bug stops reproducing.
- cargo update pushes lock-file versions to the newest allowed and can produce a huge diff. Keep dependency bumps as their own commit so a bisect can isolate them.
- cargo publish --dry-run skips only the upload — packaging, verification, and dependency checks all run for real. Released version numbers cannot be reused, so always dry-run first.
- Cross-compilation produces code for the target; linking still needs that platform toolchain (a musl linker for the Linux musl target, say). A missing toolchain fails at link time, not compile time, which reads like a source bug.
- cargo clippy --fix rewrites source in bulk and often reaches further than expected. Read the lint report first, run it from a clean commit, and review the diff.
Tips
- cargo check is much faster than cargo build - use it daily for compile checks.
- cargo clippy is the official linter - add it to CI.
- Always run cargo publish --dry-run before publishing.
- At any time you may have one mutable reference OR several immutable ones, never both.
- The ? operator works only in functions returning Result; it propagates Err upward.
- Lifetime elision covers common cases; complex ones need explicit lifetime params.
FAQ
When should I reach for cargo check instead of cargo build?
Use cargo check while iterating: it only runs type and borrow checking without emitting a binary, so it is dramatically faster than cargo build for small frequent edits. Once the code is clean, cargo build produces the debug artefact in target/debug with no optimisation, and cargo build --release is what you ship. cargo run compiles and launches in one step, with --release for checking post-optimisation performance. If incremental compilation starts producing nonsense errors, cargo clean wipes target and forces a full rebuild.
The compiler rejects two mutable borrows at once — how do I restructure?
Rust allows exactly one &mut reference or any number of & shared references at a time, never both. The classic failure is two scopes each holding &mut s, such as &mut vec[0] and &mut vec[1] at once. Fix it by taking the value out under a shared borrow, ending that borrow, and only then taking the mutable one; or by splitting borrows so disjoint fields are reached through separate variables. If neither fits, fall back to ownership transfer with let s2 = s1; and hand the value over.
How do I decide between Result and Option, and when do I reach for the ? operator?
The distinction is absence versus failure. Option<T> models a possibly missing value as Some/None, which fits a lookup that may come up empty. Result<T, E> models success or a specific error and is what you return from fn foo() -> Result<T, E> so callers can propagate failures with the ? operator, which returns early and converts the error automatically — and only works inside a function that itself returns Result. For a one-off match, if let Some(v) = opt {} or the let-else form let Ok(v) = res else { return; }; keeps the code flat.
What does cargo add give me over editing Cargo.toml by hand?
cargo add serde resolves the latest compatible version, writes it into Cargo.toml and makes the crate immediately usable without you looking up a version number. Add feature flags in the same step with cargo add serde --features derive, which enables the derive macros behind #[derive(Debug, Clone, PartialEq)]. cargo remove serde takes the dependency back out along with stale references. To move existing pinned versions forward while respecting the manifest constraints, use cargo update, which rewrites Cargo.lock without touching the manifest.
Before upgrading a dependency, how do I find out what depends on it?
Start with the forward view: cargo tree prints the whole dependency graph in tree form, exposing transitive crates and where each version comes from. To size the blast radius of one specific package, invert it with cargo tree --invert, which lists every crate that depends on the target — if that list is bigger than expected, pin the old release first and upgrade later. Once you do upgrade, run cargo check, cargo clippy, cargo fmt -- --check and cargo test in sequence to confirm nothing regressed.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us