Recoverable error rust

An error is basically an unexpected behavior or event that may lead a program to produce undesired output or terminate abruptly. Errors are things that no one wants in their program. Recoverable errors are those that do not cause the program to terminate abruptly. Example When we try to fetch

An error is basically an unexpected behavior or event that may lead a program to produce undesired output or terminate abruptly. Errors are things that no one wants in their program. Recoverable errors are those that do not cause the program to terminate abruptly. Example- When we try to fetch a file that is not present or we do not have permission to open it.

Most language does not distinguish between the two errors and use an Exception class to overcome them while Rust uses a data type Result <R,T> to handle recoverable errors and panic! macro to stop the execution of the program in case of unrecoverable errors. Result<T,E> is an enum data type with two variants OK and Err which is defined something like this:-

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

T and E are generic type parameters where T represents the type of value that will be returned in a success case within the Ok variant, and E represents the type of error that will be returned in a failure case within the Err variant.

Rust

use std::fs::File;

fn main() {

   let f = File::open("gfg.txt");

    println!("{:?}",f);

}

Output

Err(Os { code: 2, kind: NotFound, message: "No such file or directory" })

Since the file gfg.txt was not there so the Err instance was returned by File. If the file gfg.txt had been found then an instance to the file would have been returned.

If a file is not found just like the above case then it will be better if we ask the user to check the file name, file location or to give the file specifications once more or whatever the situation demands.

Rust

use std::fs::File;

fn main() {

  let f = File::open("gfg.txt");

  match f {

     Ok(file)=> {

        println!("file found {:?}",file);

     },

     Err(_error)=> {

        println!("file not found n");   

     }

      }

  }

Output

file not found

In the above program, it basically matches the return type of the result and performs the task accordingly.

Let’s create our own errors according to business logic. Suppose we want to produce an error if a person below 18 years tries to apply for voter ID.

Rust

fn main(){

  let result = eligible(13);

  match result {

     Ok(age)=>{

        println!("Person eligible to vote with age={}",age);

     },

     Err(msg)=>{

        println!("{}",msg);

     }

  }

}

fn eligible(age:i32)->Result<i32,String> {

  if age>=18 {

     return Ok(age);

  } else {

     return Err("Not Eligible..Wait for some years".to_string());

  }

}

Output

Not Eligible..Wait for some years

If we want to abort the program after it encounters a recoverable error then we could use panic! macro and to simplify the process Rust provides two methods unwrap() and expect().

  • Unwrap()

Rust

use std::fs::File;

fn main() {

   let f = File::open("gfg.txt").unwrap();

}

Output

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
Os { code: 2, kind: NotFound, message: "No such file or directory" }', main.rs:17:14

The unwrap() calls the panic! macro in case of file not found while it returns the file handler instance if the file is found. Although unwrap() makes the program shorter but when there are too many unwrap() methods in our program then it becomes a bit confusing as to which unwrap() method is called the panic! macro. So we need something that can produce the customized message. In that case, expect() method comes to the rescue.

  • expect()

Rust

use std::fs::File;

fn main() {

   let f = File::open("hello.txt").expect("Failed to open gfg.txt");

}

Output:

thread 'main' panicked at 'Failed to open gfg.txt:
Os { code: 2, kind: NotFound, message: "No such file or directory" }', main.rs:17:14

We passed our message to panic! macro via the expect parameter.

Introduction

In this article, I will discuss error handling in Rust 🦀. I try to explain the differences between recoverable and unrecoverable errors, and how to handle them properly in your code.

At the end of this article, I will also take a quick lookinto two popular crates for error handling in Rust 🦀: anyhow and thiserror.

The Panic Macro and Unrecoverable Errors

A Panic is an exception that a Rust 🦀 program can throw. It stops all execution in the current thread. Panic, will return a short description of the error and the location of the panic in the source code.

Let’s look at an example:

fn main() {
    println!("Hello, world!");
    panic!("oh no!");
}

This will print Hello, world! and then panic with the message oh no! and the location of the panic in the source code.

If your running this code in a terminal, you will see the following output:

cargo run                                                
   Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
    Finished dev [unoptimized + debuginfo] target(s) in 0.61s
     Running `target/debug/rust-error`
Hello, world!
thread 'main' panicked at 'oh no!', src/main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The message gives us also a hint on how to display a backtrace. If you run the code with the environment variable RUST_BACKTRACE=1 you will get a list of all the functions leading up to the panic.

RUST_BACKTRACE=1 cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/rust-error`
Hello, world!
thread 'main' panicked at 'oh no!', src/main.rs:3:5
stack backtrace:
   0: rust_begin_unwind
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
   2: rust_error::main
             at ./src/main.rs:3:5
   3: core::ops::function::FnOnce::call_once
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

In this case, the backtrace is not very useful, because the panic is in the main function.

Let’s look at a different example, which is extremely contrived, but for demonstration purposes, it will do.

fn a() {
    b();
}

fn b() {
    c("engin");
}

fn c(name: &str) {
    if name == "engin" {
        panic!("Dont pass engin");
    }
}

fn main() {
    a();
}

We have three functions a, b and c. The main function calls a. a calls b and b calls c. c takes a string as an argument and panics if the string is engin.

cargo run
   Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
    Finished dev [unoptimized + debuginfo] target(s) in 0.14s
     Running `target/debug/rust-error`
thread 'main' panicked at 'Dont pass engin', src/main.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

This error is not very useful. We can see that the panic happened in c, but we don’t know which function called c.

If we run the code with the environment variable RUST_BACKTRACE=1 we get the following output:

RUST_BACKTRACE=1 cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/rust-error`
thread 'main' panicked at 'Dont pass engin', src/main.rs:11:9
stack backtrace:
   0: rust_begin_unwind
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/std/src/panicking.rs:584:5
   1: core::panicking::panic_fmt
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/panicking.rs:142:14
   2: rust_error::c
             at ./src/main.rs:11:9
   3: rust_error::b
             at ./src/main.rs:6:5
   4: rust_error::a
             at ./src/main.rs:2:5
   5: rust_error::main
             at ./src/main.rs:16:5
   6: core::ops::function::FnOnce::call_once
             at /rustc/897e37553bba8b42751c67658967889d11ecd120/library/core/src/ops/function.rs:248:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

This is much better. We can see that the panic happened in c, and we can see the call stack leading up to the panic. We see that c was called by b, which was called by a, which was called by main. So let’s change the code in b to call c with a different name.

fn b() {
    c("dirien");
}

Now the code compiles and runs without any problems.

Recoverable Errors

A recoverable error is an error that can be handled by the code. For example, if we try to open a file that does not exist, we can handle the error and print a message to the user or create the file instead of crashing the program.

For this case, we can use the Result type. The Result type is an enum with two variants: Ok and Err. The Ok variant indicates that the operation was successful and stores a generic value. The Err variant indicates that the operation failed and stores an error value.

Like the Option type, the Result type is defined in the standard library, and we need to bring it into scope.

Let’s look at an example. We will try to open a file and read the contents of the file.

fn main() {
    let f = File::open("hello.txt");
}

Here we need to check the result of the open function. If the file is opened successfully, we can read the contents of the file. If the file is not opened successfully, we can print an error message to the user.

To check the result of the open function, we can use the match expression. The match expression is similar to the if expression, but it can handle more than two cases. We’re also shadowing the f variable and setting it to the match expression.

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("There was a problem opening the file: {:?}", error),
    };
}

If the open function returns Ok, we store the file handle in the f variable. If the open function returns Err, we panic and print the error message.

Let us run the code and see what happens.

cargo run
warning: `rust-error` (bin "rust-error") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rust-error`
thread 'main' panicked at 'There was a problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:7:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

We get a panic, but the error message is much more useful. We can see that the error is Os { code: 2, kind: NotFound, message: "No such file or directory" }. This error makes sense because we are trying to open a file that does not exist.

Now let’s enhance the code instead of panicking, we will create the file if it does not exist. First, we will bring the ErrorKind enum into scope.

use std::fs::File;
use std::io::ErrorKind;
...

Then we will use the match expression to check the error kind. If the error kind is NotFound, we will create the file. But the creation of the file can also fail, so we will use the match expression again to check the result of the create function. If the create function returns Ok, we will return the file handle. If the create function returns Err, we will panic.

The last part is to use other_error to handle all other errors that are not ErrorKind::NotFound.

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => panic!("There was a problem opening the file: {:?}", other_error),
        },
    };
}

Now when we run the code, we can see that no panic happens. And if we check the directory, we can see that the file was created.

cargo run
   Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
    Finished dev [unoptimized + debuginfo] target(s) in 0.62s
     Running `target/debug/rust-error`

But this code is not very readable. We have a lot of match expressions. A better way to handle this is to use closures. We will use closures to handle the Ok and Err variants of the Result type.

When we attempt to open a file, we will use the unwrap_or_else method which gives us back the file or calls the anonymous function or closure that we pass the error to. Inside the closure, we will check the error kind. If the error is NotFound then we attempt to create the file called the unwrap_or_else method again. This gives us back the file if the calls succeed. Note that we don’t have a semicolon at the end which means this is an expression and not a statement. In the failure case, we have another closure that will just panic.

fn main() {
    let f = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("There was a problem opening the file: {:?}", error);
        }
    });
}

Now we going to rewrite the code again to use the unwrap and expect methods. The unwrap method is a shortcut method that is implemented on Result types. If the Result is Ok, the unwrap method will return the value inside the Ok. If the Result is Err, the unwrap method will call the panic! macro for us.

fn main() {
    let f = File::open("hello.txt").unwrap();
}

When we run the code, we get the same error as before.

cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/debug/rust-error`
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:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The expect method is similar to the unwrap method, but we can pass a custom error message to the expect method. This error message will be printed when the Result is Err.

fn main() {
    let f = File::open("hello.txt").expect("OMG! I cant open the file!");
}

When we run the code, we can see our custom error message.

cargo run
   Compiling rust-error v0.1.0 (/Users/dirien/Tools/repos/quick-bites/rust-error)
    Finished dev [unoptimized + debuginfo] target(s) in 0.10s
     Running `target/debug/rust-error`
thread 'main' panicked at 'OMG! I cant open the file!: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:37
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

How to propagate errors

In the previous section, we saw how to handle errors. But what if we want to propagate the error to the caller of our function? This gives the caller the ability to handle the error.

Let’s say we want to read the contents of a file. We will create a function that reads username from a file. The function will return a Result type. The Result type will contain a String on success and the io::Error on error.

If the file does not exist, we will return the error. If the file exists, we will try to read the contents of the file. If this is not successful, we will return the error. If the read is successful, we will return the username.

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}

We can shorten the code by using the ? operator. The ? operator can only be used in functions that return a Result type. The ? operator is similar to our unwrap and expect methods. If the Result is Ok, the ? operator will return the value inside the Ok. If the Result is Err, instead of calling the panic! macro, the ? operator will return the error and early exit the function.

If everything is successful, the ? operator will return safely the value inside the Ok.

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;

    let mut username = String::new();

    username_file.read_to_string(&mut username)?;

    Ok(username)
}

We can shorten the code even more by chaining method calls. The ? operator can be used with method calls that return a Result type.

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}

But we can make the code even shorter by using the system module function fs::read_to_string. The fs::read_to_string function will open the file, create a new String, read the contents of the file into the String, and return it. If any of these steps fail, the fs::read_to_string function will return the error.

use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}

As mentioned before, the ? operator can only be used in functions that return a Result type. If we want to use the ? operator in the main function, we have to change the return type of the main function to Result. The main function can also return a Result type.

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;
    Ok(())
}

The main function returns a Result type. The Result type contains a () on success and a Box<dyn Error> on error.

Error helper crates

There are a lot of crates that can help you with error handling. In this section, we will look at the anyhow crate and the thiserror crate. This is not an exhaustive list of error-handling crates, but it will give you an idea of what is out there.

Of course, we can not go to deep into these crates. If you want to learn more about these crates, you can check out the links at the end of this section.

The thiserror crate

thiserror provides a derived implementation which adds the error trait for us. This makes it easier to implement the error trait for our custom error types.

To use the thiserror crate, we have to add the crate to our Cargo.toml file. The cargo add command will add the thiserror crate to our Cargo.toml file.

cargo add thiserror

We can now use the thiserror crate in our code. We will create a custom error type for our read_username_from_file function called CustomError.

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

#[derive(Debug, thiserror::Error)]
enum CustomError {
    #[error("OMG! There is an error {0}")]
    BadError(#[from] std::io::Error),

}

fn read_username_from_file() -> Result<String, CustomError> {
    let mut username = String::new();
    File::open("hello.txt")?.read_to_string(&mut username)?;
    Ok(username)
}

The anyhow crate

anyhow provides an idiomatic alternative to explicitly handling errors. It is similar to the previously mentioned error trait but has additional features such as adding context to thrown errors.

To add the anyhow crate to our project, we can use the cargo add command.

cargo add anyhow

We can now use the anyhow crate in our code. We will create a custom error type for our read_username_from_file function called CustomError.

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


fn read_username_from_file() -> Result<String, anyhow::Error> {
    let mut username = String::new();

    File::open("hello.txt").context("Failed to open file")?.read_to_string(&mut username).context("Failed to read file")?;

    Ok(username)
}

When to use thiserror and anyhow

The thiserror crate is useful when you want to implement the Error trait for your custom error types. The anyhow crate is useful when you don’t care about the error type and just want to add context to the error.

Summary

In this article, we looked at error handling in Rust 🦀. We talked about non-recoverable errors and recoverable errors. The error handling in Rust 🦀 is designed to help you in writing code that is more robust and less error-prone. The panic! macro is used for non-recoverable errors when your program is in a state where it can not continue and should stop instead of trying to proceed with invalid or incorrect data. The Result type is used for recoverable errors. The Result enums indicate that the operation can fail and that our code can recover from the error and the caller of the piece of code has to handle the success or failure of the operation.

Resources

  • Error Handling

  • The anyhow crate

  • The thiserror crate

Recoverable Errors with Result

Most errors aren’t serious enough to require the program to stop entirely.
Sometimes, when a function fails, it’s for a reason that you can easily
interpret and respond to. For example, if you try to open a file and that
operation fails because the file doesn’t exist, you might want to create the
file instead of terminating the process.

Recall from “Handling Potential Failure with Result” in Chapter 2 that the Result enum is defined as having two
variants, Ok and Err, as follows:

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

The T and E are generic type parameters: we’ll discuss generics in more
detail in Chapter 10. What you need to know right now is that T represents
the type of the value that will be returned in a success case within the Ok
variant, and E represents the type of the error that will be returned in a
failure case within the Err variant. 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.

Let’s call a function that returns a Result value because the function could
fail. In Listing 9-3 we try to open a file.

Filename: src/main.rs

{{#rustdoc_include ../listings/ch09-error-handling/listing-09-03/src/main.rs}}

Listing 9-3: Opening a file

The return type of File::open is a Result<T, E>. The generic parameter T
has been filled in by the implementation of File::open with the type of the
success value, std::fs::File, which is a file handle. The type of E used in
the error value is std::io::Error. This return type means the call to
File::open might succeed and return a file handle that we can read from or
write to. The function call also might fail: for example, the file might not
exist, or we might not have permission to access the file. The File::open
function needs to have a way to tell us whether it succeeded or failed and at
the same time give us either the file handle or error information. This
information is exactly what the Result enum conveys.

In the case where File::open succeeds, the value in the variable
greeting_file_result will be an instance of Ok that contains a file handle.
In the case where it fails, the value in greeting_file_result will be an
instance of Err that contains more information about the kind of error that
happened.

We need to add to the code in Listing 9-3 to take different actions depending
on the value File::open returns. Listing 9-4 shows one way to handle the
Result using a basic tool, the match expression that we discussed in
Chapter 6.

Filename: src/main.rs

{{#rustdoc_include ../listings/ch09-error-handling/listing-09-04/src/main.rs}}

Listing 9-4: Using a match expression to handle the
Result variants that might be returned

Note that, like the Option enum, the Result enum and its variants have been
brought into scope by the prelude, so we don’t need to specify Result::
before the Ok and Err variants in the match arms.

When the result is Ok, this code will return the inner file value out of
the Ok variant, and we then assign that file handle value to the variable
greeting_file. After the match, we can use the file handle for reading or
writing.

The other arm of the match handles the case where we get an Err value from
File::open. In this example, we’ve chosen to call the panic! macro. If
there’s no file named hello.txt in our current directory and we run this
code, we’ll see the following output from the panic! macro:

{{#include ../listings/ch09-error-handling/listing-09-04/output.txt}}

As usual, this output tells us exactly what has gone wrong.

Matching on Different Errors

The code in Listing 9-4 will panic! no matter why File::open failed.
However, we want to take different actions for different failure reasons: if
File::open failed because the file doesn’t exist, we want to create the file
and return the handle to the new file. If File::open failed for any other
reason—for example, because we didn’t have permission to open the file—we still
want the code to panic! in the same way as it did in Listing 9-4. For this we
add an inner match expression, shown in Listing 9-5.

Filename: src/main.rs

{{#rustdoc_include ../listings/ch09-error-handling/listing-09-05/src/main.rs}}

Listing 9-5: Handling different kinds of errors in
different ways

The type of the value that File::open returns inside the Err variant is
io::Error, which is a struct provided by the standard library. This struct
has a method kind that we can call to get an io::ErrorKind value. The enum
io::ErrorKind is provided by the standard library and has variants
representing the different kinds of errors that might result from an io
operation. The variant we want to use is ErrorKind::NotFound, which indicates
the file we’re trying to open doesn’t exist yet. So we match on
greeting_file_result, but we also have an inner match on error.kind().

The condition we want to check in the inner match is whether the value returned
by error.kind() is the NotFound variant of the ErrorKind enum. If it is,
we try to create the file with File::create. However, because File::create
could also fail, we need a second arm in the inner match expression. When the
file can’t be created, a different error message is printed. The second arm of
the outer match stays the same, so the program panics on any error besides
the missing file error.

Alternatives to Using match with Result<T, E>

That’s a lot of match! The match expression is very useful but also very
much a primitive. In Chapter 13, you’ll learn about closures, which are used
with many of the methods defined on Result<T, E>. These methods can be more
concise than using match when handling Result<T, E> values in your code.

For example, here’s another way to write the same logic as shown in Listing
9-5, this time using closures and the unwrap_or_else method:

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

Although this code has the same behavior as Listing 9-5, it doesn’t contain
any match expressions and is cleaner to read. Come back to this example
after you’ve read Chapter 13, and look up the unwrap_or_else method in the
standard library documentation. Many more of these methods can clean up huge
nested match expressions when you’re dealing with errors.

Shortcuts for Panic on Error: unwrap and expect

Using match works well enough, but it can be a bit verbose and doesn’t always
communicate intent well. The Result<T, E> type has many helper methods
defined on it to do various, more specific tasks. The unwrap method is a
shortcut method implemented just like the match expression we wrote in
Listing 9-4. If the Result value is the 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. Here is an example of unwrap in action:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch09-error-handling/no-listing-04-unwrap/src/main.rs}}

If we run this code without a hello.txt file, we’ll see 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

Similarly, the expect method lets us also choose the panic! error message.
Using expect instead of unwrap and providing good error messages can convey
your intent and make tracking down the source of a panic easier. The syntax of
expect looks like this:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch09-error-handling/no-listing-05-expect/src/main.rs}}

We use expect in the same way as unwrap: to return the file handle or call
the panic! macro. The error message used by expect in its call to panic!
will be the parameter that we pass to expect, rather than the default
panic! message that unwrap uses. Here’s what it looks like:

thread 'main' panicked at 'hello.txt should be included in this project: Os {
code: 2, kind: NotFound, message: "No such file or directory" }',
src/main.rs:5:10

In production-quality code, most Rustaceans choose expect rather than
unwrap and give more context about why the operation is expected to always
succeed. That way, if your assumptions are ever proven wrong, you have more
information to use in debugging.

Propagating Errors

When a function’s implementation calls something that might fail, instead of
handling the error within the function itself, you can return the error to the
calling code so that it can decide what to do. This is known as propagating
the error and gives more control to the calling code, where there might be more
information or logic that dictates how the error should be handled than what
you have available in the context of your code.

For example, Listing 9-6 shows a function that reads a username from a file. If
the file doesn’t exist or can’t be read, this function will return those errors
to the code that called the function.

Filename: src/main.rs

{{#include ../listings/ch09-error-handling/listing-09-06/src/main.rs:here}}

Listing 9-6: A function that returns errors to the
calling code using match

This function can be written in a much shorter way, but we’re going to start by
doing a lot of it manually in order to explore error handling; at the end,
we’ll show the shorter way. Let’s look at the return type of the function
first: Result<String, io::Error>. This means the function is returning a
value of the type Result<T, E> where the generic parameter T has been
filled in with the concrete type String, and the generic type E has been
filled in with the concrete type io::Error.

If this function succeeds without any problems, the code that calls this
function will receive an Ok value that holds a String—the username that
this function read from the file. If this function encounters any problems, the
calling code will receive an Err value that holds an instance of io::Error
that contains more information about what the problems were. We chose
io::Error as the return type of this function because that happens to be the
type of the error value returned from both of the operations we’re calling in
this function’s body that might fail: the File::open function and the
read_to_string method.

The body of the function starts by calling the File::open function. Then we
handle the Result value with a match similar to the match in Listing 9-4.
If File::open succeeds, the file handle in the pattern variable file
becomes the value in the mutable variable username_file and the function
continues. In the Err case, instead of calling panic!, we use the return
keyword to return early out of the function entirely and pass the error value
from File::open, now in the pattern variable e, back to the calling code as
this function’s error value.

So if we have a file handle in username_file, the function then creates a new
String in variable username and calls the read_to_string method on
the file handle in username_file to read the contents of the file into
username. The read_to_string method also returns a Result because it
might fail, even though File::open succeeded. So we need another match to
handle that Result: if read_to_string succeeds, then our function has
succeeded, and we return the username from the file that’s now in username
wrapped in an Ok. If read_to_string fails, we return the error value in the
same way that we returned the error value in the match that handled the
return value of File::open. However, we don’t need to explicitly say
return, because this is the last expression in the function.

The code that calls this code will then handle getting either an Ok value
that contains a username or an Err value that contains an io::Error. It’s
up to the calling code to decide what to do with those values. If the calling
code gets an Err value, it could call panic! and crash the program, use a
default username, or look up the username from somewhere other than a file, for
example. We don’t have enough information on what the calling code is actually
trying to do, so we propagate all the success or error information upward for
it to handle appropriately.

This pattern of propagating errors is so common in Rust that Rust provides the
question mark operator ? to make this easier.

A Shortcut for Propagating Errors: the ? Operator

Listing 9-7 shows an implementation of read_username_from_file that has the
same functionality as in Listing 9-6, but this implementation uses the
? operator.

Filename: src/main.rs

{{#include ../listings/ch09-error-handling/listing-09-07/src/main.rs:here}}

Listing 9-7: A function that returns errors to the
calling code using the ? operator

The ? placed after a Result value is defined to work in almost the same way
as the match expressions we defined to handle the Result values in Listing
9-6. If the value of the Result is an Ok, the value inside the Ok will
get returned from this expression, and the program will continue. If the value
is an Err, the Err will be returned from the whole function as if we had
used the return keyword so the error value gets propagated to the calling
code.

There is a difference between what the match expression from Listing 9-6 does
and what the ? operator does: error values that have the ? operator called
on them go through the from function, defined in the From trait in the
standard library, which is used to convert values from one type into another.
When the ? operator calls the from function, the error type received is
converted into the error type defined in the return type of the current
function. This is useful when a function returns one error type to represent
all the ways a function might fail, even if parts might fail for many different
reasons.

For example, we could change the read_username_from_file function in Listing
9-7 to return a custom error type named OurError that we define. If we also
define impl From<io::Error> for OurError to construct an instance of
OurError from an io::Error, then the ? operator calls in the body of
read_username_from_file will call from and convert the error types without
needing to add any more code to the function.

In the context of Listing 9-7, the ? at the end of the File::open call will
return the value inside an Ok to the variable username_file. If an error
occurs, the ? operator will return early out of the whole function and give
any Err value to the calling code. The same thing applies to the ? at the
end of the read_to_string call.

The ? operator eliminates a lot of boilerplate and makes this function’s
implementation simpler. We could even shorten this code further by chaining
method calls immediately after the ?, as shown in Listing 9-8.

Filename: src/main.rs

{{#include ../listings/ch09-error-handling/listing-09-08/src/main.rs:here}}

Listing 9-8: Chaining method calls after the ?
operator

We’ve moved the creation of the new String in username to the beginning of
the function; that part hasn’t changed. Instead of creating a variable
username_file, we’ve chained the call to read_to_string directly onto the
result of File::open("hello.txt")?. We still have a ? at the end of the
read_to_string call, and we still return an Ok value containing username
when both File::open and read_to_string succeed rather than returning
errors. The functionality is again the same as in Listing 9-6 and Listing 9-7;
this is just a different, more ergonomic way to write it.

Listing 9-9 shows a way to make this even shorter using fs::read_to_string.

Filename: src/main.rs

{{#include ../listings/ch09-error-handling/listing-09-09/src/main.rs:here}}

Listing 9-9: Using fs::read_to_string instead of
opening and then reading the file

Reading a file into a string is a fairly common operation, so the standard
library provides the convenient fs::read_to_string function that opens the
file, creates a new String, reads the contents of the file, puts the contents
into that String, and returns it. Of course, using fs::read_to_string
doesn’t give us the opportunity to explain all the error handling, so we did it
the longer way first.

Where The ? Operator Can Be Used

The ? operator can only be used in functions whose return type is compatible
with the value the ? is used on. This is because the ? operator is defined
to perform an early return of a value out of the function, in the same manner
as the match expression we defined in Listing 9-6. In Listing 9-6, the
match was using a Result value, and the early return arm returned an
Err(e) value. The return type of the function has to be a Result so that
it’s compatible with this return.

In Listing 9-10, let’s look at the error we’ll get if we use the ? operator
in a main function with a return type incompatible with the type of the value
we use ? on:

Filename: src/main.rs

{{#rustdoc_include ../listings/ch09-error-handling/listing-09-10/src/main.rs}}

Listing 9-10: Attempting to use the ? in the main
function that returns () won’t compile

This code opens a file, which might fail. The ? operator follows the Result
value returned by File::open, but this main function has the return type of
(), not Result. When we compile this code, we get the following error
message:

{{#include ../listings/ch09-error-handling/listing-09-10/output.txt}}

This error points out that we’re only allowed to use the ? operator in a
function that returns Result, Option, or another type that implements
FromResidual.

To fix the error, you have two choices. One choice is to change the return type
of your function to be compatible with the value you’re using the ? operator
on as long as you have no restrictions preventing that. The other technique is
to use a match or one of the Result<T, E> methods to handle the Result<T, E> in whatever way is appropriate.

The error message also mentioned that ? can be used with Option<T> values
as well. As with using ? on Result, you can only use ? on Option in a
function that returns an Option. The behavior of the ? operator when called
on an Option<T> is similar to its behavior when called on a Result<T, E>:
if the value is None, the None will be returned early from the function at
that point. If the value is Some, the value inside the Some is the
resulting value of the expression and the function continues. Listing 9-11 has
an example of a function that finds the last character of the first line in the
given text:

{{#rustdoc_include ../listings/ch09-error-handling/listing-09-11/src/main.rs:here}}

Listing 9-11: Using the ? operator on an Option<T>
value

This function returns Option<char> because it’s possible that there is a
character there, but it’s also possible that there isn’t. This code takes the
text string slice argument and calls the lines method on it, which returns
an iterator over the lines in the string. Because this function wants to
examine the first line, it calls next on the iterator to get the first value
from the iterator. If text is the empty string, this call to next will
return None, in which case we use ? to stop and return None from
last_char_of_first_line. If text is not the empty string, next will
return a Some value containing a string slice of the first line in text.

The ? extracts the string slice, and we can call chars on that string slice
to get an iterator of its characters. We’re interested in the last character in
this first line, so we call last to return the last item in the iterator.
This is an Option because it’s possible that the first line is the empty
string, for example if text starts with a blank line but has characters on
other lines, as in "nhi". However, if there is a last character on the first
line, it will be returned in the Some variant. The ? operator in the middle
gives us a concise way to express this logic, allowing us to implement the
function in one line. If we couldn’t use the ? operator on Option, we’d
have to implement this logic using more method calls or a match expression.

Note that you can use the ? operator on a Result in a function that returns
Result, and you can use the ? operator on an Option in a function that
returns Option, but you can’t mix and match. The ? operator won’t
automatically convert a Result to an Option or vice versa; in those cases,
you can use methods like the ok method on Result or the ok_or method on
Option to do the conversion explicitly.

So far, all the main functions we’ve used return (). The main function is
special because it’s the entry and exit point of executable programs, and there
are restrictions on what its return type can be for the programs to behave as
expected.

Luckily, main can also return a Result<(), E>. Listing 9-12 has the
code from Listing 9-10 but we’ve changed the return type of main to be
Result<(), Box<dyn Error>> and added a return value Ok(()) to the end. This
code will now compile:

{{#rustdoc_include ../listings/ch09-error-handling/listing-09-12/src/main.rs}}

Listing 9-12: Changing main to return Result<(), E>
allows the use of the ? operator on Result values

The Box<dyn Error> type is a trait object, which we’ll talk about in the
“Using Trait Objects that Allow for Values of Different
Types” section in Chapter 17. For now, you can
read Box<dyn Error> to mean “any kind of error.” Using ? on a Result
value in a main function with the error type Box<dyn Error> is allowed,
because it allows any Err value to be returned early. Even though the body of
this main function will only ever return errors of type std::io::Error, by
specifying Box<dyn Error>, this signature will continue to be correct even if
more code that returns other errors is added to the body of main.

When a main function returns a Result<(), E>, the executable will
exit with a value of 0 if main returns Ok(()) and will exit with a
nonzero value if main returns an Err value. Executables written in C return
integers when they exit: programs that exit successfully return the integer
0, and programs that error return some integer other than 0. Rust also
returns integers from executables to be compatible with this convention.

The main function may return any types that implement the
std::process::Termination trait, which contains
a function report that returns an ExitCode. Consult the standard library
documentation for more information on implementing the Termination trait for
your own types.

Now that we’ve discussed the details of calling panic! or returning Result,
let’s return to the topic of how to decide which is appropriate to use in which
cases.


In Rust, errors can be classified into two major categories as shown in the table below.

Sr.No Name & Description Usage
1

Recoverable

Errors which can be handled

Result enum
2

UnRecoverable

Errors which cannot be handled

panic macro

A recoverable error is an error that can be corrected. A program can retry the failed operation or specify an alternate course of action when it encounters a recoverable error. Recoverable errors do not cause a program to fail abruptly. An example of a recoverable error is File Not Found error.

Unrecoverable errors cause a program to fail abruptly. A program cannot revert to its normal state if an unrecoverable error occurs. It cannot 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.

Unlike other programming languages, Rust does not have exceptions. It returns an enum Result<T, E> for recoverable errors, while it calls the panic macro if the program encounters an unrecoverable error. The panic macro causes the program to exit abruptly.

Panic Macro and Unrecoverable Errors

panic! macro allows a program to terminate immediately and provide feedback to the caller of the program. It should be used when a program reaches an unrecoverable state.

fn main() {
   panic!("Hello");
   println!("End of main"); //unreachable statement
}

In the above example, the program will terminate immediately when it encounters the panic! macro.

Output

thread 'main' panicked at 'Hello', main.rs:3

Illustration: panic! macro

fn main() {
   let a = [10,20,30];
   a[10]; //invokes a panic since index 10 cannot be reached
}

Output is as shown below −

warning: this expression will panic at run-time
--> main.rs:4:4
  |
4 | a[10];
  | ^^^^^ index out of bounds: the len is 3 but the index is 10

$main
thread 'main' panicked at 'index out of bounds: the len 
is 3 but the index is 10', main.rs:4
note: Run with `RUST_BACKTRACE=1` for a backtrace.

A program can invoke the panic! macro if business rules are violated as shown in the example below −

fn main() {
   let no = 13; 
   //try with odd and even
   if no%2 == 0 {
      println!("Thank you , number is even");
   } else {
      panic!("NOT_AN_EVEN"); 
   }
   println!("End of main");
}

The above example returns an error if the value assigned to the variable is odd.

Output

thread 'main' panicked at 'NOT_AN_EVEN', main.rs:9
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Result Enum and Recoverable Errors

Enum Result – <T,E> can be used to handle recoverable errors. It has two variants − OK and Err. T and E are generic type parameters. T represents the type of the value that will be returned in a success case within the OK variant, and E represents the type of the error that will be returned in a failure case within the Err variant.

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

Let us understand this with the help of an example −

use std::fs::File;
fn main() {
   let f = File::open("main.jpg"); 
   //this file does not exist
   println!("{:?}",f);
}

The program returns OK(File) if the file already exists and Err(Error) if the file is not found.

Err(Error { repr: Os { code: 2, message: "No such file or directory" } })

Let us now see how to handle the Err variant.

The following example handles an error returned while opening file using the match statement

use std::fs::File;
fn main() {
   let f = File::open("main.jpg");   // main.jpg doesn't exist
   match f {
      Ok(f)=> {
         println!("file found {:?}",f);
      },
      Err(e)=> {
         println!("file not found n{:?}",e);   //handled error
      }
   }
   println!("end of main");
}

NOTE − The program prints end of the main event though file was not found. This means the program has handled error gracefully.

Output

file not found
Os { code: 2, kind: NotFound, message: "The system cannot find the file specified." }
end of main

Illustration

The is_even function returns an error if the number is not an even number. The main() function handles this error.

fn main(){
   let result = is_even(13);
   match result {
      Ok(d)=>{
         println!("no is even {}",d);
      },
      Err(msg)=>{
         println!("Error msg is {}",msg);
      }
   }
   println!("end of main");
}
fn is_even(no:i32)->Result<bool,String> {
   if no%2==0 {
      return Ok(true);
   } else {
      return Err("NOT_AN_EVEN".to_string());
   }
}

NOTE − Since the main function handles error gracefully, the end of main statement is printed.

Output

Error msg is NOT_AN_EVEN
end of main

unwrap() and expect()

The standard library contains a couple of helper methods that both enums − Result<T,E> and Option<T> implement. You can use them to simplify error cases where you really do not expect things to fail. In case of success from a method, the «unwrap» function is used to extract the actual result.

Sr.No Method Signature & Description
1 unwrap

unwrap(self): T

Expects self to be Ok/Some and returns the value contained within. If it is Err or None instead, it raises a panic with the contents of the error displayed.

2 expect

expect(self, msg: &str): T

Behaves like unwrap, except that it outputs a custom message before panicking in addition to the contents of the error.

unwrap()

The unwrap() function returns the actual result an operation succeeds. It returns a panic with a default error message if an operation fails. This function is a shorthand for match statement. This is shown in the example below −

fn main(){
   let result = is_even(10).unwrap();
   println!("result is {}",result);
   println!("end of main");
}
fn is_even(no:i32)->Result<bool,String> {
   if no%2==0 {
      return Ok(true);
   } else {
      return Err("NOT_AN_EVEN".to_string());
   }
}
result is true
end of main

Modify the above code to pass an odd number to the is_even() function.

The unwrap() function will panic and return a default error message as shown below

thread 'main' panicked at 'called `Result::unwrap()` on 
an `Err` value: "NOT_AN_EVEN"', libcoreresult.rs:945:5
note: Run with `RUST_BACKTRACE=1` for a backtrace

expect()

The program can return a custom error message in case of a panic. This is shown in the following example −

use std::fs::File;
fn main(){
   let f = File::open("pqr.txt").expect("File not able to open");
   //file does not exist
   println!("end of main");
}

The function expect() is similar to unwrap(). The only difference is that a custom error message can be displayed using expect.

Output

thread 'main' panicked at 'File not able to open: Error { repr: Os 
{ code: 2, message: "No such file or directory" } }', src/libcore/result.rs:860
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Errors in Rust are nothing but logics or syntaxes which are illegal and cause an interruption in the normal working of the program.

There are two different types of errors, which are:

  • Recoverable Errors : This kind of error can be handled by the compiler or by the user. And thus, the program execution can flow normally, after the necessary steps are taken to overcome the error.
  • Non-recoverable Errors : This kind of error can be handled by the user, which means that the user can not recover from the error. There are no ways with the help of which one can recover from the errors. And so, the program execution will stop in the error.

This has to be noted that, in other programming languages there are methods where we can use exceptions for handling errors. But in Rust, there is no such facility. Rust handles errors in a different way.

  • For recoverable errors, it returns an enum — Result<T,E>.
  • For non-recoverable errors, it will call the macro panic!().

This kind of error can be handled by the user, which means that the user can not recover from the error. When this kind of error occurs, then the macro panic! is automatically invoked by the Rust compiler and the program execution stops.

It may be noted that the macro can also be called from the user’s side also.

Program to show the use of the macro panic!()
fn main() {
   panic!("Hello");
   }

In the above program output, we are have intentionally invoked the macro panic()! to see how the program execution stops.

Program Output :

thread 'main' panicked at 'Hello', rust-error-panic.rs:2:4
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

In the next example, we shall see how an error will automatically call the macro panic!(). Here, we shall refer to an array index that is not present.

A program which abort the program(macro panic!() is called in the background automatically)

fn main() {
   let a = [1,2,3];
   a[4]; 
}

In the above program, we have declared an array whose size of 3. This array has an index starting from 0 to 2. Now when we tried to print the element from an index 4, which is not present, we are creating an error for the program flow and it is non-recoverable. Therefore, the macro panic()! is called to abort the program.

Program Output :

error: index out of bounds: the len is 3 but the index is 4
 --> rust-error-panic-calling.rs:3:4
  |
3 |    a[4];
  |    ^^^^
  |
  = note: #[deny(const_err)] on by default

error: aborting due to previous error
Using panic!() intentionally

This can be explained by considering the example below, where we use the macro panic!(), if the program flow is transferred in an else block of an if-else program.

fn main() {
    let number = 123;
    if number == 123 {
      println!("Number is same");
      }
      else {
        panic!("Number is not same. Panic is invoked");
      }
    }

Program Output :

thread 'main' panicked at 'Number is not same. Panic is invoked', rust-error-panic-invoke.rs:7:9
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

Recoverable Errors are those errors that can be handled by the compiler. When this kind of error occurs, then the enum — Result <T,E>.

The enum Result <T, E> has two variants- T and E which are generic parameters. If in case, there are no errors then the variant T will be returned. Otherwise, the variant E will be returned.

Syntax :

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

Program to search for a file

use std::fs::File;
fn main() {
  let file = File::open("my_pic01.jpg");
  match file {
    Ok(file) => {
        println!("file found {:?}", file);
      },
      Err(err) => {
        println!("file not found {:?}", err);
      }
  }
  println!("Hello! The program is still in flow.The error was handled properly");
}

In the above program we are willing to search for a file, which is not present inside the directory. And thus it is an error. Now the way to handle the error is to execute the Err part, and so that the error is handled and then the program flows normally.

Program Output :

file not found
Os { code: 2, kind: NotFound, message: "The system cannot find the file specified." }
Hello! The program is still in flow.The error was handled properly
unwrap()

The function unwrap() returns a panic if an operation fails along with printing the default error message. Otherwise, it will return the actual result.

A program to check whether a number is even

fn main() {
  let result = check(8).unwrap();
  println!("Our check returned a {} result", result);
}
fn check(no: i32) - > Result < bool, bool > {
  if no % 2 == 0 {
    return Ok(true);
  } else {
    return Err(false);
  }
}

In the above program, we have concatenated the function unwrap(), with the function in operation check(). The function check(), returns an enum that has two default variants- OK and Err. When the check functions return a true, that is when the number is even, then the OK part is executed and the keyword true is returned.

On the other hand, when the check() function returns a false, then the macro unwrap() is automatically invoked to print the error message.

Program Output :

Case1 : When the number is even (TRUE case)
rust-error-unwrap-true

Program Output :

Case 1: When the number is not even (FALSE case)

thread 'main' panicked at 'called `Result::unwrap()` on
an `Err` value: false', srclibcore
esult.rs:997:5
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
expect()

Using the function expect(), we can generate custom message for an error. This is helpful, because the compiler generated messages are somewhat very technical and may not be always understood by everyone.

A program to check whether an integer is even (FALSE Case)

fn main() {
  let result = check(81).expect("Error, this is a custom error message");
  println!("Our check returned a {} result", result);
  println!("Hello, there was no error. Let us continue..");
}
fn check(no: i32) -> Result < bool, bool > {
  if no % 2 == 0 {
    return Ok(true);
  } else {
    return Err(false);
  }
}

Program Out (When the function check() returns a false)

C:UsersHPcheckersrc>rust-error-expect
thread 'main' panicked at 'Error, this is a custom error message: false', srclibcore
esult.rs:997:5
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

0 results

An error is an unexpected behavior or event in a program that will produce an unwanted output.

In Rust, errors are of two categories:

  • Unrecoverable Errors
  • Recoverable Errors

Unrecoverable Errors in Rust

Unrecoverable errors are errors from which a program stops its execution. As the name suggests, we cannot recover from unrecoverable errors.

These errors are known as panic and can be triggered explicitly by calling the panic! macro.

Let’s look at an example that uses the panic! macro.

Example 1: Rust Unrecoverable Errors with panic! Macro

fn main() {
    println!("Hello, World!");

    // Explicitly exit the program with an unrecoverable error
    panic!("Crash");
}

Output

Hello, World!
thread 'main' panicked at 'Crash', src/main.rs:5:5

Here, the call to the panic! macro causes an unrecoverable error.

thread 'main' panicked at 'Crash', src/main.rs:5:5

Notice that the program still runs the expressions above panic! macro. We can still see Hello, World! printed to the screen before the error message.

The panic! macro takes in an error message as an argument.


Example 2: Rust Unrecoverable Errors

Unrecoverable errors are also triggered by taking an action that might cause our code to panic. For example, accessing an array past its index will cause a panic.

fn main() {
    let numbers = [1, 2 ,3];

    println!("unknown index value = {}", numbers[3]);
}

Error

error: this operation will panic at runtime
 --> src/main.rs:4:42
  |
4 |     println!("unknown index value = {}", numbers[3]);
  |                                          ^^^^^^^^^^ index out of bounds: the length is 3 but the index is 3
  |

Here, Rust stops us from compiling the program because it knows the operation will panic at runtime.

The array numbers does not have a value at index 3 i.e. numbers[3].


Recoverable Errors

Recoverable errors are errors that won’t stop a program from executing. Most errors are recoverable, and we can easily take action based on the type of error.

For example, if you try to open a file that doesn’t exist, you can create the file instead of stopping the execution of the program or exiting the program with a panic.

Let’s look at an example,

use std::fs::File;

fn main() {
    let data_result = File::open("data.txt");

// using match for Result type let data_file = match data_result { Ok(file) => file, Err(error) => panic!("Problem opening the data file: {:?}", error), };

println!("Data file", data_file); }

If the data.txt file exists, the output is:

Data file: File { fd: 3, path: "/playground/data.txt", read: true, write: false }

If the data.txt file doesn’t exist, the output is:

thread 'main' panicked at 'Problem opening the data file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23

The Result Enum

In the above example, the return type of the File::open('data.txt') is a Result<T, E>.

The Result<T, E> type returns either a value or an error in Rust. It is an enum type with two possible variants.

  • Ok(T) → operation succeeded with value T
  • Err(E) → operation failed with an error E

Here, T and E are generic types. To know more about generics or generic types, visit Rust Generics.

The most basic way to see whether a Result enum has a value or error is to use pattern matching with a match expression.

// data_file is a Result<T, E>
match data_result {
    Ok(file) => file,
    Err(error) => panic!("Problem opening the data file: {:?}", error),
 };

When the result is Ok, this code will return the file, and when the result is Err, it will return a panic!.

To learn more about pattern matching, visit Rust Pattern Matching.


The Option Enum

The Option type or Option<T> type is an enum type just like Result with two possible variants.

  • None → to indicate failure with no value
  • Some(T) → a value with type T

Let’s look at an example,

fn main() {
    let text = "Hello, World!";
    
    let character_option = text.chars().nth(15);
    

// using match for Option type let character = match character_option { None => "empty".to_string(), Some(c) => c.to_string() };

println!("Character at index 15 is {}", character); }

Output

Character at index 15 is empty

Here, the method text.chars().nth(15) returns an Option<String>. So, to get the value out of the Option, we use a match expression.

In the example above, the 15th index of the string text doesn’t exist. Thus, the Option type returns a None which matches the "empty" string.

None => "empty".to_string() 

If we were to get the 11th index of the string text, the Option enum would return Some(c), where c is the character in the 11th index.

Let’s update the above example to find out the 11th index in the string.

fn main() {
    let text = "Hello, World!";
    

let character_option = text.chars().nth(11);

// using match for Option type let character = match character_option { None => "empty".to_string(), Some(c) => c.to_string() }; println!("Character at index 11 is {}", character); }

Output

Character at index 11 is d

Difference between Result and Option enum in Rust

Option enum can return None, which can indicate failure.

However, sometimes it is essential to express why an operation failed. Thus, we have the Result enum, which gives us the Err with the reason behind the failure of the operation.

In short,

  • Option is about Some or None (value or no value)
  • Result is about Ok or Err (result or error result)

Понравилась статья? Поделить с друзьями:
  • Reconnecting error code hcnetsdk dll 800
  • Reconnect error no address rust что делать
  • Reconnect error no address rust лицензия
  • Recognizer other error 2
  • Recognition sensor error ict