As every developer knows, errors tend to happen more often than not during application development. Error handling is a fundamental concept in most programming languages that helps developers find and address errors in a program before it is shipped to end users, thereby improving the UX.
Thankfully, Rust is known for its commitment to reliability and support for error handling. In this article, we’ll explore how Rust enables developers to find and handle errors in their code, clarifying the difference between a panic! and an error. Let’s get started!
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Result enum and recoverable errorsResult <T,E> type helper methodspanic! macro and unrecoverable errors
Throwing errors is common in other programming languages, meaning that when there is an error in the code, the subsequent processes won’t be executed until they are handled correctly via try...catch.
However, in Rust, there is no such thing as try...catch. For context, we’ll review an example of how other programming languages typically handle errors using try...catch and later show how Rust would distinguish a panic! from an error with another example:
async function insertLaptop({
storageSize,
model,
ram,
isRefurbished
}) {
try {
if (isRefurbished) {
throw new Error("Cannot insert a Refurbished laptop");
}
const result = await db.laptop.add({ storage, model,ram });
return { success: true, id: result.id };
} catch(error) {
return { success: false, id: undefined };
}
}
In the JavaScript code snippet above, the insertLaptop function adds a new instance of a laptop to the database as long as the laptop is not refurbished. The function is wrapped with a try...catch error-handling mechanism. The try statement allows you to define a block of code to be tested for errors while it is being executed, whereas the catch statement allows you to define a block of code to be executed if an error occurs in the try block.
When an error occurs, JavaScript will normally stop and generate an error message with some information about what caused the error. The technical term for this is that JavaScript will throw an exception. This prevents the code from completely crashing in the event that there is an error. This is the common practice among most programming languages. However, in Rust, panic! and errors handle such exceptions.
To better understand errors in Rust, we need to understand how Rust groups its errors. Unlike most programming languages that do not distinguish between types of errors, Rust groups errors into two major categories, recoverable and unrecoverable errors. Additionally, Rust does not have exceptions. It has the type Result <T,E> for recoverable errors and the panic! macro that stops further execution when the program encounters an unrecoverable error.
Result enum and recoverable errorsIn Rust, most errors fall into the recoverable category. Recoverable errors are not serious enough to prompt the program to stop or fail abruptly since they can be easily interpreted and therefore handled or corrected. For recoverable errors, one should report the error to the user to retry the operation or offer the user an alternative course of action.
A good example is when a user inputs an invalid email. The error message might be something along the lines of please insert a valid email address and retry. The Result type or Result enum is typically used in Rust functions to return an Ok success value or an error Err value, as seen in the following example:
enum Result<T,E> {
OK(T),
Err(E)
}
T and E are generic type parameters. Ok(T) represents the return value when the case is successful and contains a value, whereas Err(E) represents an error case and returns an error value. It’s important to note that we return Result from functions when the errors are expected and recoverable.
Because Result has these generic type parameters, we can use the Result type and the functions defined on it in many different situations where the successful value and error value we want to return may differ:
use std::fs::File;
fn main() {
let f = File::open("main.jpg");
//this file does not exist
println!("{:?}",f);
}
Err(Error { repr: Os { code: 2, message: "No such file or directory" } })
The code above is a simple program to open a specified file. The program returns OK(File) if the file already exists and Err if the file is not found. For this error and other types of errors where the value type is Result <T,E>, there is a recoverable error. It allows us to access the error value without having to break the whole program.
Let’s revisit the previous code snippet. The code will panic! regardless of why File::open failed. To better debug our program, we may need to take different steps depending on the reason for failure. For example, if the code fails due to a non-existent file, we can create the file and return the new one.
If File::open fails for another reason, such as a user lacking permission to open the specified file, we want the code to panic!, as it did in the previous code snippet. To achieve this result, we add an inner match expression:
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let logo_image = File::open("main.jpg");
let logo_file = match logo_image {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("main.jpg") {
Ok(fc) => fc,
Err(e) => panic!("We encountered a problem creating this file: {:?}", e),},
other_error => {
panic!("There was a problem opening the specified file: {:?}", other_error);
}
},
};
}
In the code snippet above, we import io::ErrorKind, which contains io::Error, a struct from Rust’s standard library. io::Error is the type of value returned by File::open inside the Err variant and has a method called kind. It contains several variants that represent the different kinds of errors that might result from an io operation. In our case, we get ErrorKind::NotFound, which implies that the file we’re trying to open does not exist.
The function also has an inner match expression on error.kind(). We are checking the condition, meaning whether the value returned by error.kind() is the NotFound variant of the ErrorKind enum. If this condition is met, a new file called main.jpg is created using File::create. File::create can also fail, so we include another option for the inner match expression.
If File::create fails, we print a different error message. The second option of the outer match stays the same, implying that the program will panic when it encounters any error besides the NotFound variant of the ErrorKind enum.
Result <T,E> type helper methodsThere are other Result <T, E> type helper methods that you can use to communicate intent better than match.
unwrapThe unwrap method is better suited for more specific tasks. In our previous example, we can use unwrap. If the value is an Ok variant, unwrap will return the value inside the Ok. If the Result is the Err variant, unwrap will call the panic! macro for us. We’ll use our example from above to show unwrap in action:
use std::fs::File;
fn main() {
let logo_image = File::open("main.jpg").unwrap();
}
If we run this code without a main.jpg file, we encounter an error message from the panic! call that the unwrap method makes:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:4:49
expectexpect is another helper method that works like unwrap but makes it possible to customize the panic! error message. Let’s see expect in action:
use std::fs::File;
fn main() {
let logo_file = File::open("main.jpg")
.expect("main.jpg file should be included in the project for the program to run");
}
In the code above, we used expect instead of unwrap. It returns the file handles or calls the panic! macro in case of an error. The difference is that the error message will be the parameter passed to expect as opposed to the default panic! message that unwrap uses.
Let’s look at the output in case we call a non-existent main.jpg file:
thread 'main' panicked at 'main.jpg file should be included in the project for the
program to run: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:5:10
panic! macro and unrecoverable errorsUnrecoverable errors, which cannot be handled, are most often the result of a bug. When they occur, unrecoverable errors cause the program to abruptly fail. Additionally, the program cannot revert to its normal state, retry the failed operation, or undo the error. An example of an unrecoverable error is trying to access a location beyond the end of an array x.
In Rust, a panic! is a macro that terminates the program when it comes across an unrecoverable error. In simpler terms, panic! is referring to a critical error. The panic! macro allows a program to terminate immediately and provides feedback to the caller of the program.
panic!There are two ways to trigger a panic!. The first option is to manually panic! a Rust program. The second is by taking an action that causes the code to panic!.
To manually panic! the Rust program, we use the panic! macro. Invoke it as if you were triggering a function:
fn main() {
panic!("panic vs. error in rust"); // because of panic, the program will terminate
//immediately.
println!("I am the unreachable statement ");
}
In the example above, we’ve included a panic! Macro. Once the program encounters it, it terminates immediately:
--> hello.rs:3:5
|
2 | panic!("panic vs. error in rust");// because of panic, the program will terminate immediately.
| --------------------------------- any code following this expression is unreachable
3 | println!("I am the unreachable statement ");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unreachable statement
|
= note: `#[warn(unreachable_code)]` on by default
= note: this warning originates in the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
warning: 1 warning emitted
Below is a conditional statement that invokes the panic! macro:
fn main() {
let no = 112;
//try with no above 100 and below 100
if no>=100 {
println!("Thank you for inserting your number, Registration is successful ");
} else {
panic!("NUMBER_TOO_SHORT");
}
println!("End of main");
}
Below is the output when the variable is less than 100:
thread 'main' panicked at 'NUMBER_TOO_SHORT', hello.rs:7:8 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The output below is when the variable is greater than or equal to 100:
Thank you for inserting your number, Registration is successful End of main
The example below shows an instance of the panic! call coming directly from a library as a result of a bug as opposed to being manually called. The code below attempts to access an index in a vector. The error is due to the vector being beyond the range of valid indexes:
fn main() {
let f = vec![1, 2, 3];
v[40];
}
The program is attempting to access the 41st element of our vector ( the vector at index 40), but the vector has only three elements. In this situation, Rust will panic!. Using [] is supposed to return an element, but if you pass an invalid index, there’s no element that Rust could return here that would be correct:
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 40', hello.rs:15:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
When deciding whether your code should panic!, we need to consider the possibility of our code ending up in a bad state. A bad state is when some assumption, guarantee, contract, or invariant has been broken, such as when invalid values, contradictory values, or missing values are passed to your code, plus one or more of the following:
When a user passes in values to our code that don’t make sense, it’s advisable to return an error where possible. This gives the user the option of deciding what should be their next course of action.
However, in some cases, continuing could prove harmful or insecure. In such cases, it’s best to call panic! and alert the user that there is a bug, prompting them to fix it during the development stage.
Another appropriate case to call panic! would be when you’re calling external code that is out of your control and it returns an invalid state that you have no way of fixing. However, it’s more appropriate to return a Result than to make a panic! when failure is expected.
Similar to other programming languages, panic! provides a stack trace or backtrace
of an error. However, the backtrace is not displayed on the terminal unless the RUST_BACKTRACE environment variable is set to a value different than zero. Therefore, if you execute the following command statement in the previous code example:
RUST_BACKTRACE=1 ./hello
RUST_BACKTRACE=1 cargo run thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 40', hello.rs:15:5 stack backtrace: 0: rust_begin_unwind at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/std/src/panicking.rs:584:5 1: core::panicking::panic_fmt at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/core/src/panicking.rs:142:14 2: core::panicking::panic_bounds_check at /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/core/src/panicking.rs:84:5 3: <usize as core::slice::index::SliceIndex<[T]>>::index 4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index 5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index 6: hello::main 7: core::ops::function::FnOnce::call_once note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
All in all, Rust has two kinds of errors, an error value returned from the Result type, and an error generated from triggering the panic! macro. The Result type returns a recoverable error, or in other words, an error that doesn’t cause the program to terminate.
On the other hand, the panic! macro triggers an error that is unrecoverable or that causes the Rust program to terminate immediately.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket lets you replay user sessions, eliminating guesswork around why bugs happen by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings — compatible with all frameworks.
LogRocket's Galileo AI watches sessions for you, instantly identifying and explaining user struggles with automated monitoring of your entire product experience.
Modernize how you debug your Rust apps — start monitoring for free.

Compare the top AI development tools and models of November 2025. View updated rankings, feature breakdowns, and find the best fit for you.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the November 5th issue.

A senior developer discusses how developer elitism breeds contempt and over-reliance on AI, and how you can avoid it in your own workplace.

Examine AgentKit, Open AI’s new tool for building agents. Conduct a side-by-side comparison with n8n by building AI agents with each tool.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now