Skip to lesson

learningrust.org / basics / 01-introduction · lesson 1 of 26

TL;DR

What is Rust? Discover its speed and memory safety, then write and run your first Rust program in the browser. A free beginner lesson with no install.

Key concepts

  • learn Rust
  • Rust for beginners
  • Rust programming language
  • Rust getting started
  • why learn Rust

Introduction to Rust

TL;DR: Rust is a systems programming language that guarantees memory safety without a garbage collector. In this lesson you'll write your first Rust program, learn why ownership makes Rust different, and meet the abstractions that cost nothing once you compile with optimizations on. Estimated: 15 minutes. → Jump to code examples

Welcome to your first lesson in Rust programming! In this introduction, we'll cover what Rust is, why it's unique, and what makes it an excellent choice for modern software development.

What You'll Learn

By the end of this lesson you will have written and run a Rust program that binds a few values, does some arithmetic on them, and prints a small report — and you will have met the one arithmetic rule that catches almost everyone on day one, that dividing two whole numbers throws the fraction away instead of rounding it. That single rule is why a program can compile, run, exit cleanly, and still print a number that is quietly one too small.

The track ends with a command-line task manager called taskwork, which reads a file of tasks and prints counts and summaries of them. Every count it prints is whole-number arithmetic of exactly the kind you meet here, so getting the truncation rule into your hands now is what stops those totals being wrong later. There is no prerequisite for this lesson — this is where you start, and everything it uses is introduced on this page.

What is Rust?

Rust is a systems programming language that focuses on safety, concurrency, and performance. It began as a Mozilla Research project and is now governed independently by the Rust project and the Rust Foundation. It has topped Stack Overflow's annual developer survey as the most admired language for several years running.

Your First Rust Program

Every programming journey starts with "Hello, World!" — hit Run to see Rust in action:

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

That's it! Every Rust program starts with a fn main() function — it's the entry point. The println! macro (the ! means it's a macro, not a regular function) prints text to the screen.

Try it! Change the message inside the quotes and run it again.

Key Features

Memory Safety Without Garbage Collection

fn main() {
    // No mut here, because nothing ever reassigns name. Immutable is the
    // default precisely so that this is the shorter thing to write.
    let name = "Rust";

    // mut is needed ONLY because the next line reassigns count. Writing mut
    // on a binding you never change is a warning, not a free safety margin.
    let mut count = 0;
    count += 1;

    println!("Learning {} - Count: {}", name, count);
}

Once it runs, try one named change and predict it first: replace let mut count = 0; with let count = 0;, leaving count += 1; in place. Predict what happens before running. It stops compiling with error[E0384]: cannot assign twice to immutable variable, and rustc points at both the binding and the assignment — the two halves it needs to see to be sure. That error is the whole argument for immutability-by-default in one line: the compiler notices the contradiction between how you declared the value and how you used it.

Two habits for reading any rustc message, worth forming on this one because it is small. The error[E0384] at the front is a stable identifier: it means the same thing in every Rust program ever compiled, rustc --explain E0384 prints the general write-up for it, and unlike the prose wording it does not drift between compiler versions — so it is the part to search for. And the message is laid out in labelled parts, each with a job: --> says where, as file, line and column; the ^^^ carets mark the exact span objected to, often narrower than the line, and the narrowness is itself information; a note: states a constraint the compiler is working from; a help: offers a suggestion. Read help: as a suggestion and not an instruction — rustc is very good at proposing a change that makes an error go away, and the change that silences an error is not always the change that fixes your program.

Abstractions Designed to Cost Nothing

Rust gives you high-level features that, when you compile with optimizations on, collapse down to the same low-level code you would have written by hand. Worth knowing from day one: the sandbox behind the Run button compiles without optimizations, so what you run here still pays for those abstractions.

fn main() {
    // A tuple, not an array, because the two halves mean different things
    // (an x and a y) and are reached by position: point.0 and point.1.
    let point = (10, 20);
    println!("x = {}, y = {}", point.0, point.1);

    // An array, not a tuple, because these are five of the SAME kind of
    // thing. That is what buys .len() and indexing by a computed number.
    let numbers = [1, 2, 3, 4, 5];
    println!("First number: {}", numbers[0]);
    println!("Array length: {}", numbers.len());
}

Once it runs, try one named change and predict it first: add a sixth number to the numbers array and run it again without touching anything else. Predict what the Array length: line will say. It prints 6 — the length is read off the type rather than tracked by you, which is the point of the fixed size being known at compile time.

Arithmetic on Whole Numbers

Arithmetic looks the same as it does everywhere else, with one difference worth meeting on day one: the type of the values decides which arithmetic runs. When both sides of a / are whole numbers, Rust does whole-number division — it truncates toward zero, throwing the fractional part away rather than rounding it.

That is a rule you can predict with, not just recognise. To work out any whole-number division, ask how many complete times the divisor fits and discard the rest: 7 / 2 is 3 (not 3.5 and not 4), 9 / 10 is 0, and -7 / 2 is -3, because truncating toward zero shortens the magnitude rather than rounding down. The leftover is never rounded back in, which is why a total built from divided parts can come up short. If you want the fraction, at least one side has to be a decimal number before the division happens — converting the answer afterwards is too late, because the fraction is already gone.

Work out all three printed lines below before you run it.

Predict

Three lengths are added up and then divided by 3. Predict all three printed lines before running.

fn main() {
  let lengths = [7, 4, 2];

  let mut total = 0;
  total += lengths[0];
  total += lengths[1];
  total += lengths[2];

  let average = total / 3;
  let remaining = total - average * 3;

  println!("total = {}", total);
  println!("average = {}", average);
  println!("remaining = {}", remaining);
}
Continue learning

The answer is total = 13, average = 4, remaining = 1. Adding 7 + 4 + 2 gives 13, and 13 / 3 runs as whole-number division: it truncates to 4 and throws the leftover away entirely — it does not round to 5. remaining recovers that discarded 1 by working backwards. The rule to remember is that the operand types decide which arithmetic runs, so a fraction has to be introduced before the division, never after. The next lesson picks this up in detail when it covers numeric types and conversions.

Assembling the Calculation Yourself

Reading a rule and reconstructing one are different skills. The five lines below are the same shape of calculation, shuffled.

Arrange the code

These five lines share out a tray of slices and print how many each guest gets and how many are left. They have been shuffled. Put them in the order that runs and prints '5 each, 1 left' — then answer the question the order is really asking, which is not about names. Three of these arrangements compile; only one is right. Find the pair that can be swapped WITHOUT the compiler objecting, and say what goes wrong anyway.

  1. let each = slices / guests;
  2. slices += guests;
  3. println!("{} each, {} left", each, slices - each * guests);
  4. let guests = slices / 4;
  5. let mut slices = 13;
Continue learning

When the Program Is Right but the Answer Is Not

The truncation rule matters most when nothing goes visibly wrong. The program below compiles without a single warning, exits cleanly, and prints a percentage that is simply false.

Debug

This shares 13 slices between 3 guests and then reports what fraction of the tray each guest received. The first two lines it prints are correct. The third says each guest got 0% of the tray, which cannot be right when they each got 4 of 13 slices. Nothing crashes and nothing warns. Commit a hypothesis about which operation runs first and what it produces before you change anything, then fix it so the last line reports 30.

fn main() {
  let total_slices = 13;
  let guests = 3;

  let each = total_slices / guests;
  let leftover = total_slices - each * guests;

  let share_percent = each / total_slices * 100;

  println!("each guest gets {} slices", each);
  println!("{} slices are left over", leftover);
  println!("that is {}% of the tray each", share_percent);
}

Expected output: each guest gets 4 slices 1 slices are left over that is 30% of the tray each

Continue learning

Why Learn Rust?

  1. Safety: Rust's ownership system prevents common bugs at compile-time
  2. Performance: No garbage collector, and abstractions that optimize away in release builds
  3. Modern Tooling: Excellent package manager (Cargo) and documentation
  4. Growing Ecosystem: Active community and increasing adoption

Getting Started

To start coding in Rust, you'll need to:

  1. Install Rust using rustup
  2. Set up your development environment
  3. Learn the basic syntax — covered in Variables and Control Flow
  4. Understand Rust's ownership model

We'll cover each of these topics in detail in the upcoming lessons.

Functions in Rust

Rust lets you define your own functions to organize code. Functions can take parameters and return values.

The two fences above carried a note on every decision. This one carries none on purpose — the two choices worth explaining are both in the signatures, and stating them yourself is the point. Before you read past the fence, answer two questions: why does greet take name: &str rather than name: String, and why does the last line of add have no semicolon?

fn greet(name: &str) -> String {
    format!("Hello, {}! Welcome to Rust.", name)
}

fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    let message = greet("learner");
    println!("{}", message);

    let result = add(5, 7);
    println!("5 + 7 = {}", result);
}

Now the answers, so you can check them. greet takes name: &str rather than String because it only needs to read the text in order to format it — it borrows a view of the caller's value instead of demanding ownership of it, and the caller still has its string afterwards. And add's last line is a + b with no semicolon because that missing semicolon is exactly what makes the line the function's value rather than a discarded statement; Rust functions return their last expression without a return keyword, provided you leave the semicolon off. Both of these choices get a lesson of their own later — borrowing in Ownership, and the expression rule in Functions.

Once it runs, try one named change and predict it first: put a semicolon on the end of a + b so the line reads a + b;. Predict what the compiler says before running. It refuses to compile with error[E0308]: mismatched types, reporting expected i32, found () and offering to remove the semicolon — because with the semicolon the body no longer produces a value, and the -> i32 promise is broken.

Try It Yourself

Reading about arithmetic is not the same as getting it right under your own name. This is a build task: a small program that reports its own pass/fail. Three functions are stubbed out — one adds three readings together, one splits a total into whole shares, and one works out what is left over. Run it as-is and it fails immediately, naming the first check that did not pass. Implement each function until every check passes and it prints All checks passed.

The checks are assert_eq! calls inside main. A failing assert_eq! panics and prints both values it compared, so the first failure tells you exactly which function is still a stub and what it should have returned. The third function is the one to think about: build it from the second rather than dividing again, so that the leftover and the share can never disagree with each other.

Build

Finish the build. Three functions are stubbed out and the checks below them fail until each one returns the right value. Run it as-is to see which check fails first, work out what that function owes its caller, then implement all three until it prints 'All checks passed.' The checks run top to bottom, so the first failure you see is TODO 1 — implement it first, then work down.

// TODO 1: return the total of the three readings added together.
//   total_of(7, 4, 2) -> 13
fn total_of(a: i32, b: i32, c: i32) -> i32 {
  let _ = a;
  let _ = b;
  let _ = c;
  0
}

// TODO 2: return how many whole units each of share_count people gets
//   out of total. Whole-number division, so the fraction is discarded.
//   whole_share(13, 3) -> 4, whole_share(12, 3) -> 4
fn whole_share(total: i32, share_count: i32) -> i32 {
  let _ = total;
  let _ = share_count;
  0
}

// TODO 3: return what is LEFT OVER after handing out those whole units.
//   Build it from whole_share rather than recomputing the division.
//   left_over(13, 3) -> 1, left_over(12, 3) -> 0
fn left_over(total: i32, share_count: i32) -> i32 {
  let _ = total;
  let _ = share_count;
  0
}

// --- Build checks: Do not edit below this line. ---
fn main() {
  assert_eq!(total_of(7, 4, 2), 13, "7 + 4 + 2 is 13");
  assert_eq!(total_of(0, 0, 5), 5, "0 + 0 + 5 is 5");

  assert_eq!(whole_share(13, 3), 4, "13 split 3 ways gives 4 whole units each, not 4.33");
  assert_eq!(whole_share(12, 3), 4, "12 split 3 ways is exactly 4 with nothing left");

  assert_eq!(left_over(13, 3), 1, "13 minus the 12 handed out leaves 1");
  assert_eq!(left_over(12, 3), 0, "12 splits evenly, so nothing is left over");

  println!("All checks passed.");
  println!("Each share: {}", whole_share(13, 3));
  println!("Left over: {}", left_over(13, 3));
}

Expected output: All checks passed. Each share: 4 Left over: 1

Continue learning

Once it passes, try two variations and predict each before running:

  1. Divide twice instead of reusing the share. In left_over, replace the body with total - (total / share_count) * share_count. Predict whether any check fails before running. None does — it prints All checks passed. just the same, because that is the same arithmetic written out longhand. The reason to prefer the version that calls whole_share is not correctness today but coupling: change how a share is worked out and only one of the two versions follows the change automatically.
  2. Ask for a share of zero people. Change the last log line to whole_share(13, 0). Predict whether it prints, warns, or crashes before running. It compiles without complaint, prints All checks passed., and then panics at run time with attempt to divide by zero, so the final two log lines never appear. Division by zero is not something the type system can rule out for you, which is a first look at the difference between what the compiler checks and what only running can reveal.

Carrying the Rule Somewhere New

The truncation rule is not a fact about slices and guests. It is a fact about what happens whenever two whole numbers meet a /, and the surface it shows up on changes constantly.

Transfer

You have seen that dividing two whole numbers discards the fraction rather than rounding it. Now apply that same rule in a setting this lesson never showed you: a program works out an average rating from 7 total stars given across 2 reviews, writing let average = 7 / 2;, and separately works out a precise average with let precise = 7.0 / 2.0;. Which statement correctly predicts both, and identifies what actually decides the difference?

Continue learning

Check Yourself

Every later lesson ends with a question that reaches back to something you learned earlier. This is the first lesson, so there is nothing behind it yet — this one reaches back into what you have already read on this page. Close the page, or scroll away from the examples, and answer from memory rather than by looking.

Recall

Without scrolling up: the memory-safety example bound a name with a plain let and a counter with let mut, then ran count += 1 on the counter. Later, a function add had the single-line body a + b with no semicolon and no return. Which statement matches what those two examples showed?

Continue learning

Both halves are worth fixing in memory now. Rust variables are immutable by default and mut is how you opt out — so let name = "Rust"; is fixed for good, while let mut count = 0; may be reassigned. And a function's last expression is its return value when you leave the semicolon off, which is why add gives back a + b without ever writing return. Put the semicolon back and the function returns nothing, breaking its -> i32 promise. Everything else in this track builds on those two facts.

Key Takeaways

  • Rust is a systems programming language focused on safety, concurrency, and performance
  • It guarantees memory safety without a garbage collector through its ownership system
  • Variables are immutable by default — you opt into mutability with mut
  • Rust catches many common bugs at compile time, before your code ever runs
  • The language has excellent tooling: Cargo (package manager), rustfmt (formatter), and clippy (linter)

Next Steps

You've seen what makes Rust different: compile-time safety, immutability by default, and abstractions that optimize away in release builds. But we've only scratched the surface of how Rust thinks about data. In the next lesson, we'll explore variables and data types in depth — how Rust's type system decides what a value is, what numeric, boolean, and character types are available, and how to convert between them safely. Understanding the type system is essential because it's the foundation that makes all of Rust's safety guarantees possible.

Two-tier handoff: this document is the complete reading surface. Continue learning for stateful practice, progress, and real sandbox execution.