Step 01 of 17

Getting started

Install Rust with rustup, create and run a project with Cargo, and learn to read the compiler's error messages.

14 min 7 sections

Rust gives you the speed and control of C and C++ with a guarantee neither offers: if your program compiles (and you haven't written unsafe), it has no use-after-free, no double free and no data races. There's no garbage collector to pause it. The price is a stricter compiler, and the way to pay it cheaply is to treat that compiler as a collaborator. This lesson installs the toolchain, walks through a Cargo project, explains what happens when you build, and teaches you to read the errors you'll spend your first weeks with.

Why Rust, and when not

Most mainstream languages make you choose. Go, Java, C# and Python manage memory for you with a garbage collector (or reference counting), which is safe but costs memory and adds pauses. C and C++ hand you raw control and fast binaries, and with them a long history of security bugs: roughly 70% of the serious vulnerabilities Microsoft and Google report in their C/C++ code are memory-safety bugs.

Rust takes a third route. Every value has exactly one owner, and the compiler tracks when that owner goes away. It inserts the code that frees memory at that point, at compile time. It also checks that no reference outlives the data it points to, and that nothing is mutated while something else is reading it. You'll learn these rules in the ownership and borrowing lessons.

What you get:

  • Performance on par with C++: no runtime, no GC, zero-cost abstractions (iterators and generics compile down to the loops you'd write by hand).
  • Reliability: no null (you use Option), no exceptions (you use Result), and exhaustive pattern matching.
  • One toolchain: build, test, format, lint, docs and dependency management all ship with it.

What it costs: a steeper first month, and slower compile times than Go. If you're writing a small CRUD service and your team knows Go or TypeScript, Rust may not pay for itself. It shines in CLIs, networked services with tight latency or memory budgets, embedded systems, WebAssembly, databases, and anything you'd otherwise write in C++.

Installing the toolchain

Install Rust with rustup, the official installer and version manager. Don't use your OS package manager: its Rust is usually old, and rustup manages several toolchains side by side.

bash
# macOS / Linux
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Windows: download and run rustup-init.exe from rustup.rs

That installs a set of tools into ~/.cargo/bin:

ToolWhat it does
rustcThe compiler. You'll rarely call it directly.
cargoBuild tool and package manager. You'll call it constantly.
rustfmt / cargo fmtThe standard code formatter.
clippy / cargo clippyA linter with hundreds of checks.
rustupInstalls and updates toolchains and components.
bash
rustc --version        # rustc 1.9x.0 (...)
rustup update          # move to the latest stable (a new one ships every 6 weeks)
rustup component add rust-analyzer   # language server for your editor

Install the rust-analyzer extension in your editor (VS Code, Zed, Neovim, JetBrains RustRover has its own). It shows inferred types inline and runs cargo check on save. With Rust, seeing types you didn't write is worth a lot.

Your first Cargo project

bash
cargo new hello
cd hello
text
hello/
├── Cargo.toml
├── .gitignore
└── src/
    └── main.rs

Cargo.toml is the manifest: the package's name, version, edition and dependencies.

toml
[package]
name = "hello"
version = "0.1.0"
edition = "2024"

[dependencies]

src/main.rs holds the entry point:

rust
fn main() {
    println!("Hello, world!");
}

fn declares a function. main is where a binary starts. println! ends with ! because it's a macro, not a function: it's expanded at compile time, which is how it can take a variable number of arguments and check the format string against them.

Run it:

text
$ cargo run
   Compiling hello v0.1.0 (/home/you/hello)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/hello`
Hello, world!

The commands you'll use every day:

CommandWhat it does
cargo checkType-checks without producing a binary. The fastest feedback loop.
cargo buildDebug build into target/debug/.
cargo runBuild, then run. Arguments after -- go to your program.
cargo build --releaseOptimized build into target/release/. Often 10–50× faster at runtime.
cargo testBuild and run every test.
cargo fmt / cargo clippyFormat and lint.
cargo add serdeAdd a dependency to Cargo.toml.

Formatting values

println! and its siblings (print!, eprintln!, format!) use {} placeholders:

rust
fn main() {
    let name = "Ada";
    let year = 1843;
    let langs = vec!["Rust", "Go"];

    println!("{name} wrote the first program in {year}"); // inline variables
    println!("{} + {} = {}", 2, 2, 2 + 2);                 // positional
    println!("{langs:?}");                                  // Debug format: ["Rust", "Go"]
    println!("{:>8}|{:<8}|{:^8}|", "right", "left", "mid"); // alignment
    println!("{:.3}", std::f64::consts::PI);               // 3.142

    let s = format!("{name}-{year}"); // build a String instead of printing
    eprintln!("this goes to stderr: {s}");
}

{} uses the Display trait, which is for user-facing output; numbers and strings have it. {:?} uses Debug, which is for developers; almost every standard type has it, and you can add it to your own types with #[derive(Debug)]. Use {:#?} for a pretty-printed, multi-line version.

Reading compiler errors

The compiler will reject a lot of your code at first. Rust's error messages are unusually good, and reading them properly is the most useful skill in this lesson. Take this program:

rust
fn main() {
    let count = 0;
    count += 1;
    println!("{count}");
}
text
error[E0384]: cannot assign twice to immutable variable `count`
 --> src/main.rs:3:5
  |
2 |     let count = 0;
  |         ----- first assignment to `count`
3 |     count += 1;
  |     ^^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut count = 0;
  |         +++

Read it in this order:

  1. The headline and code. E0384 tells you the kind of error. rustc --explain E0384 prints a full explanation with examples.
  2. The location. src/main.rs:3:5 is file, line and column.
  3. The labels. ^^^ marks the primary problem; --- marks related places (here, where the variable was declared).
  4. The help:. Often a literal diff to apply. Here: add mut.

Two habits make this much easier. First, fix the first error first: later errors are often caused by earlier ones. Second, run cargo check constantly instead of waiting to build the whole thing. When an error mentions a type you don't recognize, hover over the variable in your editor to see what rust-analyzer inferred.

Practice

  1. Install Rust with rustup and add rust-analyzer to your editor.
  2. Create a project with cargo new greet.
  3. Make main read the first command-line argument and print Hello, <name>!, or Hello, world! if there's none. Use std::env::args(), which returns an iterator of Strings whose first item is the program's own path. Look up Iterator::nth with your editor's hover docs.
  4. Run it with cargo run -- Grace.
  5. Break it on purpose: try to assign to an immutable variable, and call a function that doesn't exist. Read both errors in full, and run rustc --explain on one of the codes.
  6. Run cargo fmt and cargo clippy, then build with --release and compare the file sizes in target/debug and target/release.
Show solution
rust
use std::env;

fn main() {
    // nth(1) skips the program path at index 0.
    let name = env::args().nth(1).unwrap_or_else(|| String::from("world"));
    println!("Hello, {name}!");
}
text
$ cargo run -q -- Grace
Hello, Grace!
$ cargo run -q
Hello, world!

nth(1) returns an Option<String>: Some(name) if there was an argument, None otherwise. unwrap_or_else takes the value out of Some, or calls the closure to build a default for None. You'll meet Option properly in the structs and enums lesson.

The release binary is usually smaller than the debug one because it carries no debug info, and much faster for anything that does real work.

Key takeaways

  • Rust gets memory safety from compile-time ownership rules instead of a garbage collector, so it's fast, predictable and safe.
  • Install with rustup; use Cargo for everything: check, run, test, build --release, fmt, clippy, add.
  • Each crate picks an edition in Cargo.toml; this roadmap uses 2024 (Rust 1.85+).
  • println! is a macro; {} uses Display, {:?} uses Debug.
  • Read errors top to bottom — headline, location, labels, help: — and fix the first one first.

Check yourself

Quiz

0/4 correct · 4 left

  1. 1.Which command gives you the fastest feedback that your code compiles?

  2. 2.Why does println! end with an exclamation mark?

  3. 3.A colleague says Rust is slow and shows a benchmark of target/debug/app. What's the most likely problem?

  4. 4.What does the 2024 edition in Cargo.toml mean?

Finished reading?

Mark it complete to fill in your roadmap.