# Getting started

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

Rust roadmap · Foundations · 14 min read · https://levelcode.org/roadmaps/rust/getting-started

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](https://levelcode.org/roadmaps/rust/ownership) and [borrowing](https://levelcode.org/roadmaps/rust/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`:

| Tool | What it does |
| --- | --- |
| `rustc` | The compiler. You'll rarely call it directly. |
| `cargo` | Build tool and package manager. You'll call it constantly. |
| `rustfmt` / `cargo fmt` | The standard code formatter. |
| `clippy` / `cargo clippy` | A linter with hundreds of checks. |
| `rustup` | Installs 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.

> [!NOTE]
> Rust releases a new stable version every six weeks and never breaks code that compiled before. Larger changes ship as **editions** (2015, 2018, 2021, 2024) that each crate opts into in `Cargo.toml`. Crates on different editions link together fine. This roadmap uses the 2024 edition, which needs Rust 1.85 or newer.

## 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:

| Command | What it does |
| --- | --- |
| `cargo check` | Type-checks without producing a binary. The fastest feedback loop. |
| `cargo build` | Debug build into `target/debug/`. |
| `cargo run` | Build, then run. Arguments after `--` go to your program. |
| `cargo build --release` | Optimized build into `target/release/`. Often 10–50× faster at runtime. |
| `cargo test` | Build and run every test. |
| `cargo fmt` / `cargo clippy` | Format and lint. |
| `cargo add serde` | Add a dependency to `Cargo.toml`. |

> [!WARNING]
> Never benchmark a debug build. Debug builds skip optimizations and add overflow checks, and the gap with `--release` is huge. "Rust is slow" posts are often debug builds.

## 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.

> [!TIP]
> When the borrow checker rejects something, it's usually right. Before reaching for a workaround (`.clone()` everywhere, `unsafe`), ask what could go wrong if it allowed the code. The answer is normally a real bug in some other program shape.

## 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 `String`s 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`.

**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](https://levelcode.org/roadmaps/rust/structs-and-enums).

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.

## Quiz

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

   A. cargo build --release
   B. cargo run
   C. cargo check
   D. cargo test

   **Answer: C.** cargo check runs the type and borrow checker but skips code generation, so it's much faster than building. --release is the slowest, because it optimizes.

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

   A. It's a macro expanded at compile time
   B. It can panic
   C. It writes to stdout without buffering
   D. It's an unsafe function

   **Answer: A.** The ! marks a macro invocation. Being a macro lets println! accept any number of arguments and check them against the format string when you compile.

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

   A. The edition is too old
   B. They measured an unoptimized debug build
   C. Rust's garbage collector ran during the benchmark
   D. rustup installed the wrong compiler

   **Answer: B.** Debug builds skip optimizations and add overflow checks. Benchmark with cargo build --release. Rust has no garbage collector.

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

   A. The crate only runs on Rust 2024, the final version
   B. The crate can't depend on crates from older editions
   C. The crate opts into the 2024 edition's language changes, and still links with crates on other editions
   D. The crate gets security fixes until 2024

   **Answer: C.** Editions are opt-in per crate and interoperate: a 2024 crate can use a 2018 crate and vice versa. The 2024 edition needs Rust 1.85 or newer.
