Rust: Panic vs Error – What is the difference?

In most programming languages, there is a way to programmatically throw errors or an application crashing due to an unexpected error. While this same concept applies in Rust, it is not enough to talk about an error, but also to panic. As weird as it sounds when there is panic in Rust it means an error has occurred. In this article, we will discuss the differences between panic and error in Rust.

Recoverable and Unrecoverable Errors

In Rust, there is a clear distinction between recoverable and unrecoverable errors, but before we move forward with these concepts, this might sound confusing at first if you have a background in any other programming language such as JavaScript or C# as an error is understood to simply be an error. For instance, let’s look at the following JavaScript snippet of code.

async function insertCar({
   brand,   
   model,
   year,
   isUsed
}) {
  try {
    if (isUsed) {
       throw new Error("Cannot insert used cars");
    }

    const result = await db.car.add({ brand, model, year });
    return { success: true, id: result.id };
  } catch(error) {
    return { success: false, id: undefined };
  } 
}

Notice the insertCar function adds a new car record to the database as long as the car is not used. Otherwise, throw an error.

One important aspect to notice is the logic of this function is wrapped within a try/catch. A try/catch is an error-handling mechanism to prevent the code from completely crashing when there is an error. While this is a common practice among many programming languages, there is no try/catch in Rust.

If you pay close attention to our previous snippet of code, there are two possible sources of error. A custom error is triggered by a custom logic or an error triggered by the code.

    if (isUsed) {
       // generate custom error
       throw new Error("Cannot insert used cars");
    }
    
    // an error could happen if there is not a connection
    // established with the database
    const result = await db.car.add({ brand, model, year });

One good question to ask is: Does the custom error throw new Error("Cannot insert used cars");, really need to crash the whole program in case there is not a try/catch?

Chances are there is no need to crash the whole program. One good indicator of this is that we are returning an object with the properties success and id from executing the insertCar function. We could infer that whoever calls this function will check if the car was successfully added to the database or not by checking the returned value of the property success.

In the world of Rust, we would define this kind of error as a recoverable error. As the name suggests, recoverable errors do not cause the program to terminate completely, and the code logic is properly set to handle these scenarios when encountering these kinds of errors.

However, since the try/catch is required in the snippet of code to prevent the program crashing, there wouldn’t be a way to determine if this is an error that our code logic correctly handles. What if we get an error from the database saying that the year must be a number in the case we pass a string value? Should we terminate the whole process? Should we continue running the rest of the code?

With the try/catch, we are under the assumption that all errors are unrecoverable. Unrecoverable errors cause a program to terminate immediately. Hence, it is common to use that error-handling mechanism if there is more logic that needs to be processed regardless of whether, i.e., it correctly inserted a new car record in the database or not.

What is an error in Rust?

To understand what an error is in Rust, it is recommended to look at the Result type first, as you can see below:

pub enum Result<T, E> {
    Ok(T),
    Err(E),
}

The Result type is typically used in Rust functions to return a success (Ok) value or an error (Err) value, which we can see with the following example.

fn get_name(data: &str) -> Result<&str, &str> {
    match !data.is_empty() {
        true => Ok(data),
        false => Err("Data is empty"),
    }
}

The get_name function returns the data when the data parameter is not empty, or an error value if data is empty. Pretty straightforward.

Let’s assume another function calls the get_name function which prints the returned value.

fn main() {
    let my_str = "";
    let name = get_name(&my_str);

    if name.is_ok() {
        print!("It is Ok {}", name.ok().unwrap());
    } else {
        print!("It is an error value {}", name.err().unwrap());
    }
}

Typically, in other programming languages, it is common to think of throwing errors. This means when there is an error in the code, all the subsequent processes will not be executed unless it is handled correctly via try/catch. However, there is no such thing as a try/catch in Rust.

Whenever there is a value type Result<T, E>, there is a recoverable error that allows to access the error value without the need to break the whole program. In the previous example, the program will print “It is an error value Data is empty” as my_str string literal is empty. In other programming languages, we wouldn’t have reached past defining the value for the variable name.

What is a Panic in Rust?

A panic in Rust more specifically panic!() is a macro that terminates the program when it comes across with an unrecoverable error. In other words, talking about panic is referring to a critical error.

What can trigger a panic?

Panics could be originated by using methods such as unwrap when attempting to extract the value of an Option that is None.

fn main() {
    let option_with_value: Option<&str> = Some("What up");
    let option_without_value: Option<&str> = None;

    option_with_value.unwrap(); // it returns the string literal "'
    option_without_value.unwrap(); // it panics as there's is no value
}

fn get_name(data: &str) -> Result<&str, &str> {
    match !data.is_empty() {
        true => Ok(data),
        false => Err("Data is empty"),
    }
}

Remember this example used to explain the Result<T, E>? You can check it out below:

fn main() {
    let my_str = "";
    let name = get_name(&my_str);

    if name.is_ok() {
        print!("It is Ok {}", name.ok().unwrap());
    } else {
        print!("It is an error value {}", name.err().unwrap());
    }
}

We were doing a conditional check on purpose to define whether to call the unwrap method after triggering either the ok() or the err() method. These methods are helper methods that return the Some value of its related result. For instance, if the Result is Ok(data), the ok() method will return Some(data) unless it recognizes an Err.

   #[inline]
   #[stable(feature = "rust1", since = "1.0.0")]
    pub fn ok(self) -> Option<T> {
        match self {
            Ok(x) => Some(x),
            Err(_) => None,
        }
    }

If it recognizes an Err, the ok() method returns None. If we try to use the unwrap method on a None value, it will panic.

// this would panic if value is None
print!("It is Ok {}", name.ok().unwrap()); 

On the other hand, the err() method returns Some(error_message) when it recognizes the Result to be an error, or None if the result was a success (Ok).

    #[inline]
    #[stable(feature = "rust1", since = "1.0.0")]
    pub fn err(self) -> Option<E> {
        match self {
            Ok(_) => None,
            Err(x) => Some(x),
        }
    }

In a similar case, if the err() method returns None and we attempt to use the unwrap method on a None value, it will panic.

// this would panic if value is None
print!("It is an error value {}", name.err().unwrap());

How to trigger a panic?

It is possible to manually panic the Rust program. To use the panic macro, invoke it as if you were triggering a function.

fn main() {
    let name = "Andres";

    panic!();

    print!("Hello! {}", name); // code will never reach here
}

This code will fail as soon as it triggers the panic!() macro with the following message:

thread 'main' panicked at 'explicit panic'

Unfortunately, this doesn’t provide much information as to why the program panicked besides knowing it panicked in the “main” thread (or main function). For better visibility, it is possible to add a custom message to the panic.

fn main() {
    let name = "Andres";

    panic!("Example of failing to run print! macro");

    print!("Hello! {}", name); // code will never reach here
}

Now, whenever this panic triggers, the terminal will display the custom message.

thread 'main' panicked at 'Example of failing to run print! macro'

This is useful when having multiple panic statements in an application.

Viewing the strack trace or backtrace

Similar to other programming languages, panics provide a stack trace or backtrace of an error. However, the backtrace is not displayed in the terminal unless the environment variable RUST_BACKTRACE is set to a value different than 0.

Hence, if you execute the following command statement in the previous code example,

RUST_BACKTRACE=1 cargo run

You should get a stack trace similar to the following:

thread 'main' panicked at 'Example of failing to run print! macro', src\main.rs:4:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/db9d1b20bba1968c1ec1fc49616d4742c1725b4b\/library\std\src/panicking.rs:498:5
   1: core::panicking::panic_fmt
             at /rustc/db9d1b20bba1968c1ec1fc49616d4742c1725b4b\/library\core\src/panicking.rs:107:14
   2: rust_playground::main
             at .\src\main.rs:4:5
   3: core::ops::function::FnOnce::call_once
             at /rustc/db9d1b20bba1968c1ec1fc49616d4742c1725b4b\library\core\src\ops/function.rs:227:5

Remember, the value of the environment variable can be anything besides 0. All of the following statements will display the backtrace.

RUST_BACKTRACE=6 cargo run
RUST_BACKTRACE='get trace' cargo run
RUST_BACKTRACE=true cargo run
RUST_BACKTRACE=false cargo run

Conclusion

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.

Was this article helpful?

I hope this article helped you to clarify doubts and concepts of Rust, especially to those new to this fascinating programming language.

Share your thoughts by replying on Twitter of Become A Better Programmer or to personal my Twitter account.