Rust Errors, Without the Drama
A beginner-friendly conversation on Errors, Results, Options, and beyond.
This post is under construction.
TL;DR
- Rust has no exceptions: It distinguishes recoverable errors (handled with the
Result<T, E>
type) and unrecoverable errors (handled by panicking usingpanic!()
) 1. This means we must explicitly handle errors 2. Result<T, E>
enum: Represents either success (Ok(T)
) or error (Err(E)
). Use pattern matching (match
), or methods like.unwrap()/.expect()
(which panic on error) to handle these. Prefer.expect()
with a custom message 3?
operator for propagation: To propagate errors upwards without heavy syntax, use the?
operator. It returns the error to the caller if an operation fails. Only works in functions returning a compatibleResult<T, E>
(orOption
).main()
can return aResult<T, E>
and use?
Option<T>
vsResult<T, E>
:- Use
Option<T>
when the absence of a value isn’t an error (e.g. no search result) and no error info is needed. - Use
Result<T, E>
when an operation can fail in an exceptional way and we need to convey an error message or reason 5.
- Use
- When to panic: Reserve
panic!()
for truly unrecoverable bugs or invalid states (e.g. asserting invariant). If failure is expected or possible in normal operation (e.g. file not found, invalid user input…), return aResult<T, E>
instead 7. Library code should avoid panicking on recoverable errors to let the caller decide how to handle them. -
Custom error types: For complex programs or libraries, define our own custom error types (often as
enums
) to represent various error kinds in one type. Implementingstd::error::Error
(viaDisplay
andDebug
) for these types or usingBox<dyn std::error::Error>
can help integrate with the?
operator and allow different error kinds to propagate seamlessly 8 - Keep in mind
use std::fs::File; // shortcut
use std::io::Read;
pub type Result<T> = std::result::Result<T, Error>; // alias
pub type Error = Box<dyn std::error::Error>;
fn main() -> Result<()> {
let f = File::open("foo.txt")?;
let mut data = vec![];
f.File.read_to_end(&mut data)?;
Ok(())
}

Let's have a beginner-friendly conversation on Errors, Results, Options, and beyond.
Table of Contents
Why Alice and Bob?
A long time ago (2010, may be) I read a .pdf
about the N-Body Simulations. Too bad I can’t find it on the web but here is a copy of volume 1. It was based on Ruby but it was great, easy to follow etc. One thing was that it was written as a conversation between Bob and Alice. Later the code was re-written in Python and the set of .pdf
was converted into an expensive book (55$ when I bought my copy). Today (sept 2025) you can find it on AMZN for 28€.

Moving Planets Around
Last week I start reading The Basics of Bitcoins and Blockchains
(link to AMZN).

The Basics of Bitcoins and Blockchains
In part 3 there is a section Why Alice and Bob
. Believe it or not, I then discovered where they came from.
I like to write in a conversational tone, so let’s imagine a discussion between Bob and Alice and let’s see how it goes…
Introduction: Why Rust Cares About Errors
Alice: I ran my Rust code snippet and it forced me to handle an error – it wouldn’t even compile until I did! What’s going here?
Bob: The compiler (rustc
) makes sure we acknowledge and handle errors properly before our code even runs. This helps prevent crashes in production 2.
Alice: There are no exceptions at all?
Bob: Exactly. Rust doesn’t have exceptions. Instead, it has a different model for errors. Essentially, Rust groups errors into two categories: recoverable and unrecoverable 9.
- Recoverable errors are things we expect might happen and can be dealt with (like a file not found – we might just create the file or use a default). These are handled with the
Result<T, E>
type. - Unrecoverable errors are bugs on our side or unexpected conditions (like indexing past the end of an array – something’s really wrong if that happens). For these cases Rust provides the
panic!()
macro to stop the program 1.
Alice: So Result<T, E>
is for errors I can handle, and panic!()
is for the program-halting ones?
Bob: Yes!
- Think of
Result<T, E>
as Rust’s way of saying “operation might succeed or fail”. We then decide what to do if it fails. - Whereas a
panic!()
is Rust saying “I can’t deal with this,I must crash now”.
By making error handling explicit with Result
, Rust ensures we don’t just ignore errors. It won’t let us compile unless we either handle the Result<T, E>
(e.g., check for an error) or explicitly choose to crash (like using .unwrap()
which triggers a panic if there’s an error). This leads to more robust programs because we’re less likely to have an error go unnoticed.
Alice: It’s a bit scary that the program can just crash with panic!()
though.
Bob: Again, panic!()
is for cases that are not supposed to happen like an invariant being broken.And even when a panic!()
occurs, Rust will unwind the stack and cleanup (or we can opt to abort immediately). But most of the time, you’ll use Result<T, E>
for possible errors and only panic!()
on bugs. We’ll talk more about choosing between them later 7.
Alice: Ok… So Rust wants me to handle every error. This will not be fun… How do I actually do that with Result
? What does a Result<T, E>
look like?
Bob: That’s a good question. We’ll answer it by examining how Result<T, E>
works and how to use it, but before that, it’s time to recap and practice a little.
Summary – Introduction
- Rust requires we handle errors explicitly. Code that can fail must return a
Result<T, E>
(orOption
), forcing the caller to address the possibility of failure 2. - Rust distinguishes recoverable errors (e.g. file not found, invalid input – handled with
Result
) from unrecoverable errors (bugs like out-of-bounds access – handled withpanic!()
) 9. - No exceptions are used. This language design decision helps prevent unchecked errors. We either deal with the error or deliberately choose to
panic!()
/.unwrap()
, making error handling clear in the code.
Exercises – Introduction
- Identify Error Types: Think of two scenarios in programming:
- one that would be a recoverable error
- one that would be an unrecoverable error
For each scenario, explain whether we would use Rust’s
Result<T, E>
or apanic!()
, and why. - Compile-time Check: Write a Rust program that attempts to open a non-existent file with
std::fs::File::open(foo.txt)
without handling theResult
. Observe the compiler error or warning. Then, fix it by handling theResult<T, E>
(for now, we can just use a simplepanic!()
or print an error message in case ofErr
). One can read 4
Optional - Setting Up our Development Environment
Good to know
- The code is on Github
- I expect either
CodeLLDB
extension or theBuild Tools for Visual Studio
to be installed.- Both can be installed
I use VSCode under Windows 11 and I already wrote a post about my setup. I don’t know yet if I will need more than one project accompanying this post but I already know the first project does’nt have main.rs
file but multiple short sample code in the examples/
directory instead. This is fine but I want to make sure we can quickly modify, build, debug and go step by step in the source code.
Now, having this in mind here is what I do and why.
- Get the project from GitHub
- Open
/examples/ex00.rs
in the editor - At the time of writing here is what I see :

- Just for testing purpose, delete the
target/
directory if it exists - Press
CTRL+SHIFT+B
. This should build a debug version of the code- Check
target/debug/examples/
. It should containsex00.exe
- Check
- Set the cursor on line 5 then press
F9
- This set a breakpoint on line 5.

- Open de
Run & Debug
tab on the side (CTRL+SIFT+D
) - In the list select the option corresponding to our configuration (LLDB or MSVC). See below :

- Press
F5
- This starts the debug session
- If needed the application is built (not the case here)
- The execution stops on line 5. See below :

- Press
F10
to move forward- Line 5 is executed
- On the left hand side, in the Local subset, we can chack that
bob
is now equal to 5.

- Press
F5
to continue and reach the end of the code
Let’s make a last test
- Delete the
target/
directory exe00.rs
should be in the editor with a breakpoint on line 5- Press
F5
- The ex00.exe is built
- The debug session starts
- Execution is stopped on line 5 as before
The making of:
The secret ingredient lies in ./vscode/task.json
and ./vscode/launch.json
.vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "cargo-build-debug",
"type": "cargo",
"command": "build",
"args": [
"--example",
"${fileBasenameNoExtension}",
],
"problemMatcher": [
"$rustc"
],
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "cargo-build-release",
"type": "cargo",
"command": "build",
"args": [
"--release",
"--example",
"${fileBasenameNoExtension}",
],
"problemMatcher": [
"$rustc"
]
}
]
}
- In
cargo-build-debug
,group
helps to make thecargo-build-debug
task the default one (CTRL+SHIFT+B). - Note that since the source code to compile is in
examples/
directory we pass--example
and the name of the file (e.g.ex00
) as arguments. - To see the list of tasks use
ALT+T
the pressR
- Below we can see both tasks :
cargo-build-debug
andcargo-build-release
- Below we can see both tasks :

.vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"type": "cppvsdbg",
"request": "launch",
"name": "Debug (MSVC)",
"program": "${workspaceFolder}/target/debug/examples/${fileBasenameNoExtension}.exe",
"args": [],
"cwd": "${workspaceFolder}",
"environment": [
{
"name": "RUST_BACKTRACE",
"value": "short"
}
],
"preLaunchTask": "cargo-build-debug"
},
{
"type": "cppvsdbg",
"request": "launch",
"name": "Release (MSVC)",
"program": "${workspaceFolder}/target/release/examples/${fileBasenameNoExtension}.exe",
"args": [],
"cwd": "${workspaceFolder}",
"environment": [
{
"name": "RUST_BACKTRACE",
"value": "short"
}
],
"preLaunchTask": "cargo-build-release"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug (CodeLLDB)",
"program": "${workspaceFolder}/target/debug/examples/${fileBasenameNoExtension}.exe",
"args": [],
"cwd": "${workspaceFolder}",
"sourceLanguages": ["rust"],
"preLaunchTask": "cargo-build-debug"
}
]
}
- The path in
program
key points to the executable created at the end of the build (do you see${fileBasenameNoExtension}
?) - Note the
preLaunchTask
. This explains why we can press F5 (debug) even if the file is not built. In such case, the taskcargo-build-debug
is run then the debug session starts.