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,
“A language that doesn’t affect the way you think about programming, is not worth knowing.”medium.com
▸ Installation & Hello World ▸ Cargo & Crates ▸ Variable bindings , Constants & Statics ▸ Comments ▸ Functions ▸ Primitive Data Types ▸ Operators
▸ Control Flows
“If a person will spend one hour a day on the same subject for five years, that person will be an expert on that…medium.com
▸ Vectors ▸ Structs ▸ Enums ▸ Generics ▸ Impls & Traits
▸ Ownership ▸ Borrowing ▸ Lifetimes & Lifetime Elision
“Do the best you can until you know better. Then when you know better, do better.”medium.com
▸ 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 withassert_eq!()andassert_ne!()should return same data type.
We can set custom error messages for these macros as well. For examples,
- 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 andResult&Optiontypes.
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 onlyOptionandResult, 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,Optiontypes 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 useOption<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 tounwrap(), If anOptiontype hasSomevalue or aResulttype has aOkvalue, the value inside them passes to the next step. But when havingNoneorErr, the functionalities are bit different.
unwrap_or(): WithNoneorErr, the value you passes tounwrap_or()is passing to the next step. But the data type of the value you passes should match with the data type of the relevantSomeorOk.
unwrap_or_default(): WithNoneorErr, the default value of the data type of the relevantSomeorOk, is passing to the next step.
unwrap_or_else(): Similar tounwrap_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 relevantSomeorOk.
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 errorsstd::fs::File::open()can produce, check the error list onstd::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 supportxor()forOptiontypes, which returnsSomeonly if one expression gotSome, 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 languagesfilterfunctions are used with arrays or iterators to create a new array/ iterator by filtering own elements via a function/ closure. Rust also providesfilter()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 offilter()withOptiontypes.
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 languagesmap()functions are used with arrays or iterators, to apply a closure on each element of the array or iterator. Rust also providesmap()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 ofmap()withOptionandResulttypes.
map(): Convert type T by applying a closure. The data type ofSomeorOkblocks can be changed according to the return type of the closure. ConvertOption<T>toOption<U>,Result<T, E>toResult<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()forResulttypes : The data type ofErrblocks can be changed according to the return type of the closure. ConvertResult<T, E>toResult<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 forOptiontypes (not supportingResult). Apply the closure to the value insideSomeand return the output according to the closure. The given default value is returned forNonetypes.
map_or_else(): Support for bothOptionandResulttypes (Resultstill nightly only). Similar tomap_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 defaultErrmessage should pass as argument.
ok_or_else(): Similar took_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(): ConvertOption<T>toOption<&T>andResult<T, E>toResult<&T, &E>as_mut(): ConvertsOption<T>toOption<&mut T>andResult<T, E>toResult<&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 + DisplaymeansErrortrait inherits fromfmt::Debugandfmt::Displaytraits.
▸ 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 thestruct,{, 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 aStringfrom&strdata type. Actually this also an implementation ofstd::convert::Fromtrait.
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.


🚀 Hire me! 🥤 Buy me a coffee! ⭐ Contribute!

