Error Handling in Rust: try/catch and Beyond — A Complete Guide

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Error Handling in Rust: Moving Away from try/catch for Safety



Unlike Python or Java, Rust does not have a classic try/catch construct. The language designers took a different path, making error handling part of the type system. This is not a whim, but a fundamental decision that guarantees memory safety and program predictability at compile time. In this article, we will cover all ways of handling errors in Rust: from basic Result and Option to advanced methods with the ? operator and custom error types.



1. Two Main Types: Result and Option



In Rust, all errors are divided into two camps: recoverable (handled via Result) and unrecoverable (leading to panic!). There is also the Option type for cases where a value may be absent.



The Result Type



Result<T, E> is an enum that can be either Ok(T) (success with data) or Err(E) (error with description). This is the primary tool for handling expected errors: I/O errors, parsing errors, network request errors.



use std::fs::File;use std::io::Read;

fn read_file_content(path: &str) -> Result<String, std::io::Error> { let mut file = File::open(path)?; let mut content = String::new(); file.read_to_string(&mut content)?; Ok(content)}


The Option Type



Option<T> is another enum that takes the values Some(T) (value exists) or None (no value). It is used when the absence of data is a normal situation, not an error.



fn find_user(id: u32) -> Option<String> {    let users = vec!["Alice".to_string(), "Bob".to_string()];    users.get(id as usize).cloned()}

fn main() { match find_user(0) { Some(name) => println!("User found: {}", name), None => println!("User not found"), }}


2. The ? Operator — An Elegant Replacement for try/catch



If Rust had try/catch, it would be cumbersome. Instead, the language offers the ? operator. It works like this: if the result is Ok, the value is extracted and passed on; if Err, the error is immediately returned from the function.



use std::num::ParseIntError;

fn parse_and_double(input: &str) -> Result<i32, ParseIntError> { let value: i32 = input.parse()?; // if error — immediately return Err Ok(value * 2)}

fn main() { match parse_and_double("42") { Ok(result) => println!("Result: {}", result), Err(e) => println!("Parsing error: {}", e), }}


The ? operator can only be used in functions that return Result or Option. This forces the developer to explicitly indicate that the function may fail — unlike exceptions in Python, which can "pop up" from anywhere.



3. When to Use panic! and unwrap



Not all errors need to be handled. panic! is a macro for unrecoverable errors. It stops the program and unwinds the stack. Use it only in exceptional situations:



  • Critical states: violation of invariants that cannot be fixed.
  • Examples and prototypes: in educational code or one-off scripts.
  • Tests: to verify that code panics in expected places.


fn divide(a: i32, b: i32) -> i32 {    if b == 0 {        panic!("Division by zero! a={}, b={}", a, b);    }    a / b}


unwrap() and expect()

Blogs

Book Recommendations