DEV Community

sai umesh
sai umesh

Posted on

Error Handling in Rust programming language

Alt text of image

If you are following stack overflow developer survey, you might have seen Rust as most loved programming language 2 years in a row. Rust is probably most fascinating programming language I’ve seen, and I completely agree with survey.

Having said that, Learning Rust is not easy, learning curve is very high, and If you are coming from languages like JavaScript like me, it will be very challenging journey but, this tough road is worth it.

If you are very new to learning Rust, I strongly suggest starting with their docs, Rust has one of the best documentations available.

Today we will be looking at error handling in rust.

If you are coming from languages like JavaScript, the way JS and Rust handles errors are poles apart. Enough of talk let’s start with some code 😋😋.

Before coding anything we need to learn Option and Result in Rust.

Option in Rust represents an optional value: every Option is either Some and contains a value, or None, and does not. Option types are very common in Rust code, as they have a number of uses.

Result in Rust is the type used for returning and propagating errors. It is an enum with the variants, Ok(T), representing success and containing a value, and Err(E), representing error and containing an error value.

enum Result<T, E> {

 Ok(T),

 Err(E),

}

Now let’s see some examples.

First we will learn about Option

fn main() {
    // first let's define vector/array of numbers
    let numbers = vec![1, 2, 4];

    // now let's try to read value from vector
    let index_one = numbers[1];
    println!("value at index {}", index_one);

    // now let's try to read 10th index which doesn't exists
    // since Rust doesn't have neither null nor try/catch to handle error
    // so we will take leverage of **Option**

    // un comment below two line to check error 😢😢

    // let out_of_index = numbers[10];
    // println!("this will throw error {}", out_of_index);

    // above code would result in following error
    // thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 10'

    // rust has very interesting way to handle these errors
    // we will take advantage of Option and get method from
    // vector to handle these kind of errors

    // get method of vector returns **Option** enum
    // Option will have two properties Some and None
    // let's see how to read/catch error
    match numbers.get(10) {
        Some(value) => println!("Hello {}", value),
        None => println!("Yo!! out of bound error")
    }

    // now let's see how to read value from it
    let result = match numbers.get(10) {
        Some(value) => value,
        None => &-1 // defaulting to -1
    };

    println!("result is {}", result);

    // as you can see above code works well but,
    // we can improve code by taking advantage of Result
}

Now, let’s improve above code little bit

fn read_from_vec(input: &Vec<i32>, index: usize) -> Result<i32, &'static str> {
    return match input.get(index) {
        Some(value) => Ok(*value),
        None => Err("Out of bound exception")
    }
}

fn main() {
    // now lets take same example and improve error handling
    let numbers = vec![1, 2, 4];
    let result = read_from_vec(&numbers, 2);

    // now since result is of type **Result** we can handle this in multiple ways
    if result.is_ok() {
        println!("got the value {}", result.unwrap());
    } else {
        println!("yo!! got the error {:?}", result.unwrap_err());
    }

    // above code works but there's an even better way to handle the same
    match result {
        Ok(data) => println!("indexed value {}", data),
        Err(err) => println!("error is  {}", err)
    }

Now let’s see a Generic use case of reading file.

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

fn read_from_file(file_path: &'static str) -> Result<String, io::Error> {
    let mut file_data = String::new();

    // now let's try to read file
    // here ? is for handling errors
    // if there's an error it will break program and returns error
        // let mut f = File::open(file_path)?;
        // f.read_to_string(&mut file_data)?;
        // return Ok(file_data);

    // we can simplify above code by writing
    File::open(file_path)?.read_to_string(&mut file_data)?;
    Ok(file_data)

}


fn main() {
    // now let's see more generic use cases
    // for error handling

    let file_path = "hello.txt";
    match read_from_file(file_path) {
        Ok(data) => println!("file data {}", data),
        Err(err) => println!("err {}", err)
    };
}

I’m still learning Rust, Hopefully I made some sense about Error handling in Rust Language. If you have any doubts or suggestions you can contact me on my twitter. until then, I will see you in other Post. Peace ❤❤

Top comments (9)

Collapse
 
aminmansuri profile image
hidden_dude

I find that this approach to error handling is a step back.

Back in the C days we had to error check every single line (some even used macros to do it). Because your program can't continue if there's an error. So you do things like:

if(ERROR ==func1()) {
// error handle
}
if(ERROR == func2()) {
// error handle
}

It became messy with so much error handling everywhere. So some "cleaned it up" with GOTO:

if(ERROR==func1())
goto ERROR_BLOCK1;

if(ERROR==func2())
goto ERROR_BLOCK2;

// do stuff

return OK;

ERROR_BLOCK1:
//error
ERROR_BLOCK2:
// error
return ERROR;

So you start seeing a pattern where there is a happy path and an error path in your code. This evolved into the try block and the catch block and led to exceptions where an added benefit is that you don't have to have an error block in every function. (Other languages had an ON ERROR block built in)

It was refreshing to clean up error handling and taking it away from every line of code and isolating it in one place. In essence this is a form of separation of concerns that keeps the code clean.

I fail to see Rust's solution as a step forward. It seems like a regression in language design to me. But I'm happy to learn of any benefits to its approach if I'm missing something.

Collapse
 
saiumesh profile image
sai umesh

I wish I could answer this better but, unfortunately I've never worked with C/C++ so I don't know how they work. now coming to Rust, one can feel like lot of boiler code for handling errors.

 
genbattle profile image
Nick Sarten • Edited

Code like this looks super messy to me:

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

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

    let mut s = String::new();

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

Rust has a solution to this, the ? operator. It used to be a macro back in the early days, but it basically means: give me the value here if everything's ok, or return the error if not.

So your example becomes:

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

    f?.read_to_string(&mut s)?
    Ok(s)
}

The overhead here is similar to exceptions with better enforcement around handling. This is a massive improvement on C-style error return values.

Thread Thread
 
aminmansuri profile image
hidden_dude

Thanks Nick.

That makes a bit more sense to me. I'm not convinced that its better than Exceptions (though I never really loved Exceptions either).. but you're right that this makes it much better than the old C way. My only problem is that if you forget a little '?' somewhere you don't get proper error handling. Exceptions aren't as brittle.

In C we used to write macros to accomplish such things but of course they are more verbose.

Thread Thread
 
genbattle profile image
Nick Sarten • Edited

Most of the time if you forget a ? somewhere then your program won't compile, because you'll have an Option/Result, not the value you're expecting in a given expression.

You're correct though that there's a hole where functions return Result<(), Error> because you can ignore the return value without getting an error, however the Rust compiler will spit out a warning for an unused Result:

warning: unused `std::result::Result` that must be used
 --> src/lib.rs:9:5
  |
9 |     f?.read_to_string(&mut s);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: #[warn(unused_must_use)] on by default
  = note: this `Result` may be an `Err` variant, which should be handled

So while I agree that Rust isn't head and shoulders better than C++ in every way, I think error handling is currently one area where Rust has a significant advantage.

Thread Thread
 
aminmansuri profile image
hidden_dude

thanks for the info Nick.

 
aminmansuri profile image
hidden_dude • Edited

So if my function returns an error. Will the caller abort automatically? Where would it abort?

As I said: I'd like to understand better.

I don't know why its bulky boilerplate. In some programs there can be very few catch clauses in the code. Often its handled globally some way. Only when you care do you have to write some error handling.

I don't see how this is more work that Option.

As for computational expense I guess that would depend on the language. It would involve some for of long jump and stack unrolling. In itself that doesn't seem too expensive. Besides, when an exception is happening the computation part of your code is over, you're now dying, no longer doing computation.

Thread Thread
 
aminmansuri profile image
hidden_dude

So if you have the following function:

fn myFunc() {
   option opt1 = f1()
   option opt2 = f2()
   // do more
}

If f1() returns an error will that cause f2() not to execution and myFunc() to fail?

If the answer is no then I have no choice to write horrible code like:

fn myFunc() {
   option opt1 = f1()
   if (opt1.isError()) return ERROR;
   option opt2 = f2()
   if (opt2.isError()) return ERROR;
   // do more
}

Ie. we're back to prehistory of error checking where all I see is error checking code. Low signal to noise ratio.

Code like this looks super messy to me:

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

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

    let mut s = String::new();

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

The error handling is all mixed into the business logic.

Collapse
 
aminmansuri profile image
hidden_dude

You always care if its null. If something bad happens the calling function may need to abort. So the if statement is important to know if you have to abort or not.

Exceptions elegantly solve this problem. The Rust mechanism, since it doesn't interrupt execution, actually seems to make things more complex.