Podcast Title

Author Name

0:00
0:00
Album Art

A Whirlwind Tour of Rust's Core Concepts

By 10xdev team August 03, 2025

This article will take you from zero to Rust in under 10 minutes. Instead of focusing on just a few concepts, we will go through numerous Rust snippets to explain the meaning behind their keywords and symbols. This will be a fast-paced guide. After you have installed Rust from rustup.rs, you are ready to begin.

Understanding Variables and Basic Types

let introduces a variable binding. This can be written as a single line. i32 is a signed 32-bit integer. You can specify the variable's type explicitly after a colon, which is called a type annotation.

let x: i32 = 1;

Behind the scenes, languages such as Python hide the implementation from you and quietly promote the length of the integer at runtime when needed. This is clever but inefficient when you know how big your numbers are going to be. Rust, of course, has crates that provide this dynamic behavior if you need it.

Note on Integer Types: How do you choose which integer type to use in Rust? In general, if you just need to represent some number, use an i32. 32 bits has a wide enough range that it's relatively unlikely to overflow, and signed numbers have fewer surprises than unsigned numbers. i32 is equivalent to a long in C or an int in Java. If you use a numeric literal and don't explicitly annotate the type, Rust will default to i32.

If you declare a name and initialize it later, the compiler will prevent you from using it before it's initialized. While this sounds obvious, C crashes at runtime if you attempt to do this.

The underscore (_) is a special name, or rather a lack of a name. It basically means to throw away something and not warn about it not being used. This pattern of strict defaults with escape hatches to not be overly annoying is one you will see a lot with Rust.

let _ = 42; // I don't care about this value

Working with Tuples

Rust has tuples, which you can think of as fixed-length collections of values of different types. Note that the Rust compiler can nearly always infer the types that you are using, and only rarely do you need to clarify ambiguous cases. This helps prevent errors where you might think you're working with one type of variable when it's actually another.

Tuples can be destructured when doing an assignment, which means they're broken down into their individual fields. This is especially useful when a function returns a tuple.

let pair = ('a', 17);
pair.0; // 'a'
pair.1; // 17

// When destructuring, you can ignore parts of it
let (first, _) = pair;

When destructuring a tuple, an underscore can be used to throw away part of it. The semicolon marks the end of a statement; unlike in other languages, semicolons are not just mandatory whitespace.

Functions, Blocks, and Expressions

In Rust, like in Lisp, nearly everything is an expression. fn declares a function. Below, we have a void function (a function that returns nothing) and a function that returns an integer. The arrow (->) indicates a function's return type.

fn a_void_function() {
    // This function returns nothing
}

fn a_function_that_returns_an_integer() -> i32 {
    5 // The return value
}

A pair of brackets declares a block, which has its own scope, sort of like an immediately-invoked function in JavaScript. This interior variable x only lives as long as the block does and does not modify the external x.

Blocks are also expressions, which means they evaluate to a value. If you've written Lisp or Ruby, this should start to feel very familiar. Inside a block, there can be multiple statements. We call the final expression of a block the "tail," which is what the whole block will evaluate to. These are equivalent:

// Example 1
let x = {
    let y = 1;
    y + 1
}; // x is 2

// Example 2
let x = 2;

This is why omitting the semicolon at the end of a function is the same as returning a value. if conditionals are also expressions. A match is also an expression, not a statement.

Accessing Values and Namespaces

Dots (.) are typically used to access fields of a value or call a method on a value, just like in most C-style languages. The double colon (::) is similar but operates on namespaces. This distinction provides great clarity between using a property or a namespace.

In the following example, std is a crate (a library), cmp is a module (a source file), and min is a function.

std::cmp::min(a, b);

The use directive can bring names from other namespaces into scope. Rust has strict scoping rules; if you don't see it in your source code, it's not available. Types are namespaces too, and methods can be called as regular functions. str is a primitive type, but many non-primitive types are also in scope by default.

The Power of Structs and Methods

Structs are the backbone of Rust's excellent, rich type system. Think of structs as lightweight new types encapsulating the valid states of your system.

Match arms are patterns. A match has to be exhaustive—at least one arm needs to match—and an underscore can be used as a catch-all pattern. In addition to primitive integer matches, you can also match deeply nested data and destructure it for ease of use.

You can declare methods on your own types. Here, we're adding an is_positive method to our new Number struct, and then we can use our new methods like usual.

struct Number {
    value: i32,
}

impl Number {
    fn is_positive(&self) -> bool {
        self.value > 0
    }
}

Variable bindings are immutable by default, which means their interior can't be mutated, and they also cannot be reassigned. Those of you coming from functional languages like Haskell will be very pleased to see immutability by default, rather than in other C-like languages which typically have immutability added on later through keywords like const. While Rust isn't strictly a functional language, it is clear that the language design has balanced practicality with purity from the functional world.

Leveraging Generics

Functions can be generic. They can have multiple type parameters, which can then be used in the function's declaration and its body instead of concrete types. Think of them like a template string. Note that in this case, both a and b must be of the same type T.

The standard library type Vec, which is a heap-allocated array, is generic. v1 is a vector of integers, and v2 is a vector of booleans. Behind the scenes, vectors use an array and swap it out for a larger array at runtime when it reaches full capacity.

Speaking of Vec, it comes with a macro that gives us more or less Vec literals. v1 is a vector of integers (i32), as usual, and v2 is a vector of booleans. All of these invoke a macro. You can recognize them by the bang (!) at the end of the name. In fact, println! is a macro.

Robust Error Handling

panic! is also a macro. It violently stops execution with an error message and the file name and line number of the error. Some methods also panic. For example, the Option type can contain something or it can contain nothing. If unwrap is called on it and it contains nothing, it panics.

Option is not a struct; it's an enum with two variants. Note the generic type parameter: an Option::Some can be of any type. Enum's variants can be used in patterns.

enum Option<T> {
    None,
    Some(T),
}

Result is also an enum. It can either contain something or an error. It also panics when unwrapped and containing an error. Functions that can fail typically return a Result. For instance, when creating a UTF-8 string from bytes, not all bytes represent a valid string.

In the first example, we can see that s1 is the Ok variant of the Result enum, but s2 is the Err variant. This pattern of errors as values keeps us in the functional world, where other languages would have exceptions which break us out.

let s1 = std::str::from_utf8(&[240, 159, 146, 150]); // Ok("💖")
let s2 = std::str::from_utf8(&[195, 40]); // Err(...)

If you want to panic in case of failure, you can unwrap. Or you can use expect for a custom error message. It's called expect because it telegraphs to people reading both the code and the errors what you were expecting when you unwrapped the result.

You can also match and handle the error, or use if let to safely destructure the inner value if it is Ok. Alternatively, you can bubble up the error, returning it to the calling function, which then handles it.

This pattern of unwrapping the value inside a Result if it's Ok or returning it if it's an Err is so common that Rust has dedicated syntax to do it. The question mark operator (?) at the end of a line does the exact same thing as a larger match statement. This is the normal Rust error pattern in application code where you're trying to just write the happy path, though the previous options are available to you when you need them.

Powerful and Lazy Iterators

One of the most powerful features in Rust is its iterator system. Consider an iterator that represents all natural numbers from one to infinity. This is possible to store in RAM because iterators are computed lazily on demand.

let all_numbers = 1..; // Represents 1, 2, 3, ...

This iterator notation is called a range. The most basic iterators are ranges; they can be open at the bottom or top, or you can specify both ends exactly. Computation only happens when the iterator is called.

Anything that is iterable can be used in a for loop. We've just seen a range being used, but it also works with a Vec, a slice, or an actual iterator. Note that string literals also have a .bytes() iterator if you want the raw bytes. Rust's char type is a Unicode scalar value that is always a valid character.

You can use an iterator in a for loop even if the iterator items are filtered, mapped, and flattened. This fluent interface pattern is found everywhere in Rust.

A New Way of Thinking

Instead of classes and mutating shared data, modeling your program state as structs and then writing functions to move between these valid states makes invalid states unrepresentable.

Writing Rust is a very different experience from reading it. On one hand, it's more difficult; you're not just reading a solution to a problem, you're actively solving it. But on the other hand, the Rust compiler helps out a lot. The compiler always has very good error messages and insightful suggestions. And when there's a hint missing, the compiler team is not afraid to add it.

Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Recommended For You

Up Next