“An ounce of prevention is worth a pound of cure.”
__ Benjamin Franklin

RUST : ERROR HANDLING

Welcome Back! This is the fifth post of Learning Rust series at Medium. Also now, you can read the same content via learning-rust.github.io 👈 . I think it’s more structured and easy to see the big picture.

In the previous posts we discussed about,

▸ Installation & Hello World ▸ Cargo & Crates ▸ Variable bindings , Constants & Statics ▸ Comments ▸ Functions ▸ Primitive Data Types ▸ Operators 
▸ Control Flows

▸ Vectors ▸ Structs ▸ Enums ▸ Generics ▸ Impls & Traits

▸ Ownership ▸ Borrowing ▸ Lifetimes & Lifetime Elision

▸ Modules ▸ Crates ▸ Workspaces ▸ std modules and Preludes

Rust is a well designed language and safety is one of the main focus area of it. Its design decisions have been taken to prevent errors and major issues of systems programs like data races, null pointer exceptions, sensitive data leakage through exceptions and etc. So today, we are going to talk more about the concepts behind Error Handling in Rust.


Smart Compiler

The Rust compiler does the most significant job to prevent errors in Rust programs. It analyzes the code at compile-time and issues warnings, if the code does not follow memory management rules or lifetime annotations correctly. For example,

💭 In the third post; RUST : THE TOUGH PART , we have discussed memory management concepts like ownership, borrowing, lifetimes and etc.

Rust compiler checks not only issues related with lifetimes or memory management and also common coding mistakes, like the following code.

Above error messages are very descriptive and we can easily see where is the error. But while we can not identify the issue via the error message, rustc --explain commands help us to identify the error type and how to solve it, by showing simple code samples which express the same problem and the solution we have to use. For example, rustc --explain E0571 shows the following output in the console.

💡 Also you can read the same explanations via Rust Compiler Error Index . For example to check the explanation of E0571 error, you can use https://doc.rust-lang.org/error-index.html#E0571 .


Panicking

panic!()

▸ In some cases, while an error happens we can not do anything to handle it, if the error is something, which should not be happened. In other words, if it’s an unrecoverable error.
▸ Also when we are not using a feature-rich debugger or proper logs, sometimes we need to debug the code by quitting the program from a specific line of code by printing out a specific message or a value of a variable binding to understand the current flow of the program.

For above cases, we can use panic! macro. Let’s see few examples.

panic!() runs thread based. One thread can be panicked, while other threads are running.

01. Quit from a specific line.

02. Quit with a custom error message.

03. Quit with the value of code elements.

As you can see in the above examples panic!() supports println!() type style arguments . By default, it prints the error message, file path and line & column numbers where the error happens.


unimplemented!()

If your code is having unfinished code sections, there is a standardized macro as unimplemented!() to mark those routes. The program will be panicked with a “not yet implemented” error message, if the program runs through those routes.


unreachable!()

This is the standard macro to mark routes that the program should not enter. The program will be panicked with a “’internal error: entered unreachable code’” error message, if the program entered those routes.

We can set custom error messages for this as well.


assert!(), assert_eq!(), assert_ne!()

These are standard macros which usually use with test assertions.

  • assert!() ensures that a boolean expression is true. It panics if the expression is false.
  • assert_eq!() ensures that two expressions are equal. It panics if the expressions are not equal.
  • assert_ne!() ensures that two expressions are not equal. It panics if the expressions are equal.
⭐ Expressions which use with assert_eq!() and assert_ne!() should return same data type.

We can set custom error messages for these macros as well. For examples,

  1. With a custom message for assert_eq!()

2. assert_eq!() with debug data


debug_assert!(), debug_assert_eq!(), debug_assert_ne!()

🔎 These are similar to above assert macros. But these statements are only enabled in non optimized builds by default. All these debug_assert macros will be omitted in release builds, unless we pass -C debug-assertions to the compiler.


Option and Result

Many languages use null\ nil\ undefined types to represent empty outputs, and Exceptions to handle errors. Rust skips using both, especially to prevent issues like null pointer exceptions, sensitive data leakages through exceptions and etc. Instead, Rust provides two special generic enums;Option and Result to deal with above cases.

💭 In the second post; RUST : BEYOND THE BASICS , we discussed about the basics of enums, generics and Result & Option types.

As you know,
 ▸ An optional value can have either Some value or no value/ None.
 ▸ A result can represent either success/ Ok or failure/ Err

💭 Do you remember that? Not only Option and Result, and also their variants are in preludes. So, we can use them directly without using namespaces in the code. Refer RUST : LET’S GET IT STARTED! for more info.

Basic usages of Option

When writing a function or data type, 
 ▸ if an argument of the function is optional,
 ▸ If the function is non-void and if the output it returns can be empty,
 ▸ If the value, of a property of the data type can be empty
 We have to use their data type as an Option type

For example, if the function outputs a &str value and the output can be empty, the return type of the function should set as Option<&str>

Same way, if the value of a property of a data type can be empty or optional like middle_name of Name data type in the following example, we should set its data type as an Option type.

💭 As you know, we can use pattern matching to catch the relevant return type (Some/ None) via match. There is a function to get the current user’s home directory in std::env as home_dir() . Because of all users doesn’t have a home directory in the systems like Linux, home directory of the user can be optional. So it returns an Option type; Option<PathBuf> .

⭐ However, when using optional arguments with functions, we have to pass None values for empty arguments while calling the function.

🔎 Other than that, Option types are used with nullable pointers in Rust. Because of there is no null pointers in Rust, the pointer types should point to a valid location. So if a pointer can be nullable, we have use Option<Box<T>> .

Basic usages of Result

If a function can produce an error, we have to use a Result type by combining the data type of the valid output and the data type of the error. For example, if the data type of the valid output is u64 and error type is String , return type should be Result<u64, String> .

💭 As you know, we can use the pattern matching to catch the relevant return types (Ok/Err) via match. There is a function to fetch the value of any environment variable in std::env as var() . Its input is the environment variable name. This can produce an error, if we passes a wrong environment variable or the program can not extract the value of the environment variable while running. So its return type is a Result type; Result<String, VarError> .


is_some(), is_none(), is_ok(), is_err()

Other than match expressions, Rust provides is_some() , is_none() and is_ok() , is_err() functions to identify the return type.

ok(), err() for Result types

In addition to that Rust provides ok() and err() for Result types. They convert the Ok<T> and Err<E> values of a Result type to Option types.


Unwrap and Expect

unwrap()

▸ If an Option type has Some value or a Result type has a Ok value, the value inside them passes to the next step.
 ▸ If the Option type has None value or the Result type has Err value, program panics; If Err, panics with the error message.

The functionality is bit similar to the following codes, which are using match instead unwrap() .

Example with Option and match, before using unwrap()

Example with Result and match, before using unwrap()

Same codes in above main functions can be written with unwrap() using two lines.

⭐ But as you can see, when using unwrap() error messages are not showing the exact line numbers where the panic happens.


expect()

Similar to unwrap() but can set a custom message for the panics.


unwrap_err() and expect_err() for Result types

The opposite case of unwrap() and expect(); Panics with Ok values, instead Err. Both print the value inside Ok on the error message.

💡 Usually use with tests.


unwrap_or(), unwrap_or_default() and unwrap_or_else()

💡These are bit similar to unwrap(), If an Option type has Some value or a Result type has a Ok value, the value inside them passes to the next step. But when having None or Err, the functionalities are bit different.
  • unwrap_or() : With None or Err, the value you passes to unwrap_or() is passing to the next step. But the data type of the value you passes should match with the data type of the relevant Some or Ok.
  • unwrap_or_default() : With None or Err, the default value of the data type of the relevant Some or Ok, is passing to the next step.
  • unwrap_or_else() : Similar to unwrap_or(). The only difference is, instead of passing a value, you have to pass a closure which returns a value with the same data type of the relevant Some or Ok.

Error and None Propagation

We should use panics like panic!(), unwrap(), expect() only if we can not handle the situation in a better way. Also if a function contains expressions which can produce either None or Err
 ▸ we can handle them inside the same function. Or,
 ▸ we can return None and Err types immediately to the caller. So the caller can decide how to handle them.

💡None types no need to handle by the caller of the function always. But Rusts’ convention to handle Err types is, return them immediately to the caller to give more control to the caller to decide how to handle them.


? Operator

▸ If an Option type has Some value or a Result type has a Ok value, the value inside them passes to the next step.
 ▸ If the Option type has None value or the Result type has Err value, return them immediately to the caller of the function.

Example with Option type,

Example with Result type,


try!()

⭐ ? operator was added in Rust version 1.13. try!() macro is the old way to propagate errors before that. So we should avoid using this now.

▸ If a Result type has Ok value, the value inside it passes to the next step. If it has Err value, returns it immediately to the caller of the function.


Error propagation from main()

Before Rust version 1.26, we couldn’t propagate Result and Option types from the main() function. But now, we can propagate Result types from the main() function and it prints the Debug representation of the Err.

💡 We are going to discuss about Debug representations in upcoming section “Error trait” under “Custom Error Types”.
💯 If you want to know about the all kind of errors std::fs::File::open() can produce, check the error list on std::fs::OpenOptions.

Combinators

Let’s see what a combinator is,

  • One meaning of “combinator” is a more informal sense referring to the combinator pattern, a style of organizing libraries centered around the idea of combining things. Usually there is some type T, some functions for constructing “primitive” values of type T, and some “combinators” which can combine values of type T in various ways to build up more complex values of type T. The other definition is “function with no free variables” (wiki.haskell.org)
  • A combinator is a function which builds program fragments from program fragments; in a sense the programmer using combinators constructs much of the desired program automatically, rather that writing every detail by hand.
    __ John Hughes - Generalizing Monads to Arrows via Functional Programming Concepts

The exact definition of “combinators” in Rust ecosystem is bit unclear.

or(), and(), or_else(), and_then() 
 
- Combine two values of type T and return same type T.

filter() for Option types
 - Filter type T by using a closure as a conditional function.
 - Return same type T

map(), map_err() 
 - Convert type T by applying a closure
 - The data type of the value inside T can be changed. ex Some<&str> can be converted to Some<usize> or Err<&str> to Err<isize> and etc.

map_or(), map_or_else() 
 
- Transform type T by applying a closure & return the value inside type T.
 - For None and Err, a default value or another closure is applied.

ok_or(), ok_or_else() for Option types
 - Transform Option type into a Result type.

as_ref(), as_mut() 
 - Transform type T into a reference or a mutable reference.


or() and and()

While combining two expressions which return either Option/ Result,
or() : If either one got Some or Ok, that value returns immediately.
and() : If both got Some or Ok, the value in the second expression returns. If either one got None or Err that value returns immediately.

🔎 Rust nightly support xor() for Option types, which returns Some only if one expression got Some, not both.

or_else()

Similar to or(). The only difference is, the second expression should be a closure which returns same type T.


and_then()

Similar to and(). The only difference is, the second expression should be a closure which returns same type T.


filter()

💡 Usually in programming languages filter functions are used with arrays or iterators to create a new array/ iterator by filtering own elements via a function/ closure. Rust also provides filter() as an iterator adaptor to apply a closure on each element of an iterator to transform it into another iterator. However in here we are talking about the functionality of filter() with Option types.

The same Some type is returned, only if we pass a Some value and the given closure returned true for it. None is returned, if None type passed or the closure returned false. The closure uses the value inside Some as an argument. Still Rust support filter() only for Option types.


map() and map_err()

💡 Usually in programming languages map() functions are used with arrays or iterators, to apply a closure on each element of the array or iterator. Rust also provides map() as an iterator adaptor to apply a closure on each element of an iterator to transform it into another iterator. However in here we are talking about the functionality of map() with Option and Result types.
  • map() : Convert type T by applying a closure. The data type of Some or Ok blocks can be changed according to the return type of the closure. Convert Option<T> to Option<U> , Result<T, E> to Result<U, E>

⭐ Via map(), only Some and Ok values are getting changed. No affect to the values inside Err (None doesn’t contain any value at all).

  • map_err() for Result types : The data type of Err blocks can be changed according to the return type of the closure. Convert Result<T, E> to Result<T, F>.

⭐ Via map_err(), only Err values are getting changed. No affect to the values inside Ok.


map_or() and map_or_else()

Hope you remember the functionality of unwrap_or() and unwrap_or_else() functions we discussed under “Unwrap and Expect” section in this post. These functions also bit similar to them. But map_or() and map_or_else() apply a closure on Some, Ok values and return the value inside type T.

  • map_or() : Support only for Option types (not supporting Result). Apply the closure to the value inside Some and return the output according to the closure. The given default value is returned for None types.
  • map_or_else() : Support for both Option and Result types (Result still nightly only). Similar to map_or() but should provide another closure instead a default value for the first parameter.

None types doesn’t contain any value. So no need to pass anything to the closure as input with Option types. But Err types contain some value inside it. So default closure should able to read it as an input, while using this with Result types.


ok_or() and ok_or_else()

As mentioned earlier, ok_or(), ok_or_else() transform Option type into Result type. Some to Ok and None to Err .

  • ok_or() : A default Err message should pass as argument.
  • ok_or_else() : Similar to ok_or(). A closure should be passed as the argument.

as_ref() and as_mut()

🔎 As mentioned earlier, these functions are used to borrow type T as a reference or as a mutable reference.

  • as_ref() : Convert Option<T> to Option<&T> and Result<T, E> to Result<&T, &E>
  • as_mut() : Converts Option<T> to Option<&mut T> and Result<T, E> to Result<&mut T, &mut E>

Custom Error Types

Rust allow us to create our own Err types. We call them “Custom error types”.

Error trait

As you know traits define the functionality a type must provide. But we don’t need to define new traits for common functionalities always, because Rust standard library provides some reusable traits which can be implemented on our own types. While creating custom error types std::error::Error trait help us to convert any type to an Err type.

As we discussed under traits inheritance in the second post; RUST : BEYOND THE BASICS, a trait can be inherited from another traits. trait Error: Debug + Display means Error trait inherits from fmt::Debug and fmt::Display traits.

Display 
 - How should the end user see this error as a message/ user-facing output.
 - Usually print via println!("{}") or eprintln!("{}")

Debug 
 - How should display the Err while debugging/ programmer-facing output.
 - Usually print via println!("{:?}") or eprintln!("{:?}") 
 - To pretty-print, println!("{:#?}") or eprintln!("{:#?}") can be used.

source() 
 - The lower-level source of this error, if any.
 - Optional.

First, let’s see how to implement std::error::Error trait on a simplest custom error type.

Hope you understood the main points. Now, let’s see some custom error type with an error code and an error message.

⭐️ Rust standard library provides not only reusable traits and also it facilitates to magically generate implementations for few traits via #[derive] attribute. Rust support derive std::fmt::Debug, to provide a default format for debug messages. So we can skip std::fmt::Debug implementation for custom error types and use #[derive(Debug)] before struct declaration.

For a struct #[derive(Debug)] prints, the name of the struct , { , comma-separated list of each field’s name and debug value and }

From trait

When writing real programs, mostly we have to deal with different modules, different std and third party crates at the same time. But each crate uses their own error types and if we are using our own error type, we should convert those errors into our error type. There is a standardized trait we can use for these conversions, std::convert::From.

💡 As you know, String::from() function is used to create a String from &str data type. Actually this also an implementation of std::convert::From trait.

Let’s see how to implement std::convert::From trait on a custom error type.

In the above example, File::open(“nonexistent.txt”)? produces std::io::Error. But because of the return type is Result<(), AppError>, it converts to an AppError. Because of we are propagating the error from main() function, it prints the Debug representation of the Err.

In the above example we deal with only one std error type, std::io::Error. Let’s see some example which handles multiple std error types.

🔎 Search about the implementation of std::io::ErrorKind, to see how to organize error types further.

Okay, Let’s stop our fifth post of Learning Rust series in here. In this post we discussed about,

▸ Smart Compiler ▸ Panicking ▸ Option and Result ▸ Unwrap and Expect
▸ Error and None Propagation ▸ Combinators ▸ Custom Error Types

🐣 I am a Sri Lankan🇱🇰 Web Developer who works in Vietnam🇻🇳. I am not a native English speaker and I am just practicing Rust in my very little leisure time, while learning more about Golang, Devops and so much in the workplace. 
So, if you found any mistake or something I need to be changed, even a spelling or a grammar mistake, please let me know.

Also as I mentioned in the beginning, now you can read the same content via learning-rust.github.io 👈. Feel free to create a ticket or contribute to make learning Rust easier to everyone. Thanks.

👉 learning-rust.github.io 👈
🚀 Hire me! 🥤 Buy me a coffee!Contribute!