Austin Roy Omondi Live long and prosper 👌

panic! vs. error in Rust

8 min read 2519

Rust Panic Vs Error

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!

Table of contents

Error handling: Rust vs. JavaScript

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.

Errors in Rust

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 errors

In 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.

Matching on different errors

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 methods

There are other Result <T, E> type helper methods that you can use to communicate intent better than match.

Rust unwrap

The 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

Rust expect

expect 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 errors

Unrecoverable 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.

How to trigger a 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:

--&gt; 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  

Recover or stop execution

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:

  • The bad state is something that is unexpected, as opposed to something that will likely happen occasionally, like a user entering data in the wrong format
  • Your code after this point needs to rely on not being in this bad state, rather than checking for the problem at every step
  • There’s not a good way to encode this information in the types you use

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.

Viewing the stack trace or backtrace

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  

Stack trace

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. 

Summary

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.

LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

Modernize how you debug your Rust apps — .

Austin Roy Omondi Live long and prosper 👌

Leave a Reply