In the previous chapter, we learned about creating endpoints and SQL queries to handle the management of the User entity. In this chapter, we are going to learn more about error handling in Rust and Rocket. After learning the concepts in this chapter, you will be able to implement error handling in a Rocket application.
We are also going to discuss more common ways to handle errors in Rust and Rocket, including signaling unrecoverable errors using the panic! macro and catching the panic! macro, using Option, using Result, creating a custom Error type, and logging the generated error.
In this chapter, we're going to cover the following main topics:
For this chapter, we have the same technical requirements as the previous chapter. We need a Rust compiler, a text editor, an HTTP client, and a PostgreSQL database server.
You can find the source code for this chapter at https://github.com/PacktPublishing/Rust-Web-Development-with-Rocket/tree/main/Chapter07.
To understand error handling in Rust, we need to begin with the panic! macro. We can use the panic! macro when the application encounters an unrecoverable error and there's no purpose in continuing the application. If the application encounters panic!, the application will emit the backtrace and terminate.
Let's try using panic! on the program that we created in the previous chapter. Suppose we want the application to read a secret file before we initialize Rocket. If the application cannot find this secret file, it will not continue.
Let's get started:
use std::env;
let secret_file_path = env::current_dir().unwrap().join("secret_file");
if !secret_file_path.exists() {
panic!("secret does not exists");
}
thread 'main' panicked at 'secret does not exists', src/main.rs:15:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
RUST_BACKTRACE=1 cargo run
Finished dev [unoptimized + debuginfo] target(s)
in 0.18s
Running `target/debug/our_application`
thread 'main' panicked at 'secret does not exists', src/main.rs:15:9
stack backtrace:
...
14: our_application::main
at ./src/main.rs:12:36
15: core::ops::function::FnOnce::call_once
at /rustc/59eed8a2aac0230a8b5
3e89d4e99d55912ba6b35/library/core/
src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
[profile.dev]
panic = "abort"
Now that we know how to use panic!, let's see how we can catch it in the next section.
As well as using panic!, we can also use the todo! and unimplemented! macros in Rust code. Those macros are very useful for prototyping because they will call panic! while also allowing the code to type-check at compile time.
But, why does Rocket not shut down when we are calling a route with todo!? If we check the Rocket source code, there's a catch_unwind function in src::panic that can be used to capture a panicking function. Let's see that code in the Rocket source code, core/lib/src/server.rs:
let fut = std::panic::catch_unwind(move || run())
.map_err(|e| panic_info!(name, e))
.ok()?;
Here, run() is a route handling function. Each time we call a route that is panicking, the preceding routine will convert the panic into the result's Err variant. Try removing the secret_file_path routine we added before and running the application. Now, create a user and try going into user posts. For example, create a user with the 95a54c16-e830-45c9-ba1d-5242c0e4c18f UUID. Try opening http://127.0.0.1/users/95a54c16-e830-45c9-ba1d-5242c0e4c18f/posts. Since we only put todo!("will implement later") in the function body, the application will panic, but the preceding catch_unwind function will catch the panic and convert it into an error. Please note that catch_unwind will not work if we set panic = "abort" in Cargo.toml.
In a regular workflow, we don't usually want to use panic!, because panicking interrupts everything, and the program will not be able to continue. If the Rocket framework does not catch panic! and one of the route handling functions is panicking, then that single error will close the application and there will be nothing to handle the other requests. But, what if we want to terminate the Rocket application when we encounter an unrecoverable error? Let's see how we can do it in the next section.
To shut down smoothly if the application encounters an unrecoverable error in the route handling function, we can use the rocket::Shutdown request guard. Remember, the request guard is a parameter we are supplying to the route handling functions.
To see the Shutdown request guard in action, let's try implementing it in our application. Using the previous application, add a new route in src/routes/mod.rs called /shutdown:
use rocket::Shutdown;
...
#[get("/shutdown")]
pub async fn shutdown(shutdown: Shutdown) -> &'static str {
// suppose this variable is from function which
// produces irrecoverable error
let result: Result<&str, &str> = Err("err");
if result.is_err() {
shutdown.notify();
return "Shutting down the application.";
}
return "Not doing anything.";
}
Try adding the shutdown() function in src/main.rs. After that, rerun the application and send an HTTP request to /shutdown while monitoring the output of the application on the terminal. The application should shut down smoothly.
In the next two sections, let's see how we can use Option and Result as an alternative way to handle errors.
In programming, a routine might produce a correct result or encounter a problem. One classical example is division by zero. Dividing something by zero is mathematically undefined. If the application has a routine to divide something, and the routine encounters zero as input, the application cannot return any number. We want the application to return another type instead of a number. We need a type that can hold multiple variants of data.
In Rust, we can define an enum type, a type that can be different variants of data. An enum type might be as follows:
enum Shapes {
None,
Point(i8),
Line(i8, i8),
Rectangle {
top: (i8, i8),
length: u8,
height: u8,
},
}
Point and Line are said to have unnamed fields, while Rectangle is said to have named fields. Rectangle can also be called a struct-like enum variant.
If all members of enum have no data, we can add a discriminant on the member. Here is an example:
enum Color {
Red, // 0
Green = 127, // 127
Blue, // 128
}
We can assign enum to a variable, and use the variable in a function as in the following:
fn do_something(color: Color) -> Shapes {
let rectangle = Shapes::Rectangle {
top: (0, 2),
length: 10,
height: 8,
};
match color {
Color::Red => Shapes::None,
Color::Green => Shapes::Point(10),
_ => rectangle,
}
}
Going back to error handling, we can use enum to communicate that there's something wrong in our code. Going back to division by zero, here is an example:
enum Maybe {
WeCannotDoIt,
WeCanDoIt(i8),
}
fn check_divisible(input: i8) -> Maybe {
if input == 0 {
return Maybe::WeCannotDoIt;
}
Maybe::WeCanDoIt(input)
}
The preceding pattern returning something or not returning something is very common, so Rust has its own enum to show whether we have something or not in the standard library, called std::option::Option:
pub enum Option<T> {
None,
Some(T),
}
Some(T) is used to communicate that we have T, and None is obviously used to communicate that we don't have T. We used Option in some of the previous code. For example, we used it in the User struct:
struct User {
...
description: Option<String>,
...
}
We also used Option as a function parameter or return type:
find_all(..., pagination: Option<Pagination>) -> (..., Option<Pagination>), ... {}
There are many useful things we can use with Option. Suppose we have two variables, we_have_it and we_do_not_have_it:
let we_have_it: Option<usize> = Some(1);
let we_do_not_have_it: Option<usize> = None;
match we_have_it {
Some(t) => println!("The value = {}", t),
None => println!("We don't have it"),
};
if let Some(t) = we_have_it {
println!("The value = {}", t);
}
assert!(we_have_it != we_do_not_have_it);
assert!(we_have_it.is_some());
assert!(we_do_not_have_it.is_none());
assert_eq!(we_have_it.unwrap(), 1);
// assert_eq!(we_do_not_have_it.unwrap(), 1);
// will panic
assert_eq!(we_have_it.expect("Oh no!"), 1);
// assert_eq!(we_do_not_have_it.expect("Oh no!"), 1); // will panic
assert_eq!(we_have_it.unwrap_or(42), 1);
assert_eq!(we_do_not_have_it.unwrap_or(42), 42);
let x = 42;
assert_eq!(we_have_it.unwrap_or_else(|| x), 1);
assert_eq!(we_do_not_have_it.unwrap_or_else(|| x), 42);
assert_eq!(we_have_it.map(|v| format!("The value = {}", v)), Some("The value = 1".to_string()));
assert_eq!(we_do_not_have_it.map(|v| format!("The value = {}", v)), None);
assert_eq!(we_have_it.map_or("Oh no!".to_string(), |v| format!("The value = {}", v)), "The value = 1".to_string());
assert_eq!(we_do_not_have_it.map_or("Oh no!".to_string(), |v| format!("The value = {}", v)), "Oh no!".to_string());
assert_eq!(we_have_it.map_or_else(|| "Oh no!".to_string(), |v| format!("The value = {}", v)), "The value = 1".to_string());
assert_eq!(we_do_not_have_it.map_or_else(|| "Oh no!".to_string(), |v| format!("The value = {}", v)), "Oh no!".to_string());
There are other important methods, which you can check in the documentation for std::option::Option. Even though we can use Option to handle a situation where there's something or nothing, it does not convey a message of something went wrong. We can use another type similar to Option in the next part to achieve this.
In Rust, we have the std::result::Result enum that works like Option, but instead of saying we have it or we don't have it, the Result type is more about saying we have it or we have this error. Just like Option, Result is an enum type of the possible T type or possible E error:
enum Result<T, E> {
Ok(T),
Err(E),
}
Going back to the division by zero problem, take a look at the following simple example:
fn division(a: usize, b: usize) -> Result<f64, String> {
if b == 0 {
return Err(String::from("division by zero"));
}
return Ok(a as f64 / b as f64);
}
We don't want division by 0, so we return an error for the preceding function.
Similar to Option, Result has many convenient features we can use. Suppose we have the we_have_it and we_have_error variables:
let we_have_it: Result<usize, &'static str> = Ok(1);
let we_have_error: Result<usize, &'static str> = Err("Oh no!");
match we_have_it {
Ok(v) => println!("The value = {}", v),
Err(e) => println!("The error = {}", e),
};
if let Ok(v) = we_have_it {
println!("The value = {}", v);
}
if let Err(e) = we_have_error {
println!("The error = {}", e);
}
assert!(we_have_it != we_have_error);
assert!(we_have_it.is_ok());
assert!(we_have_error.is_err());
assert_eq!(we_have_it.ok(), Some(1));
assert_eq!(we_have_error.ok(), None);
assert_eq!(we_have_it.err(), None);
assert_eq!(we_have_error.err(), Some("Oh no!"));
assert_eq!(we_have_it.unwrap(), 1);
// assert_eq!(we_have_error.unwrap(), 1);
// panic
assert_eq!(we_have_it.expect("Oh no!"), 1);
// assert_eq!(we_have_error.expect("Oh no!"), 1);
// panic
assert_eq!(we_have_it.unwrap_or(0), 1);
assert_eq!(we_have_error.unwrap_or(0), 0);
assert_eq!(we_have_it.unwrap_or_else(|_| 0), 1);
assert_eq!(we_have_error.unwrap_or_else(|_| 0), 0);
assert_eq!(we_have_it.map(|v| format!("The value = {}", v)), Ok("The value = 1".to_string()));
assert_eq!(
we_have_error.map(|v| format!("The error = {}",
v)),
Err("Oh no!")
);
assert_eq!(we_have_it.map_err(|s| s.len()), Ok(1));
assert_eq!(we_have_error.map_err(|s| s.len()), Err(6));
assert_eq!(we_have_it.map_or("Default value".to_string(), |v| format!("The value = {}", v)), "The value = 1".to_string());
assert_eq!(we_have_error.map_or("Default value".to_string(), |v| format!("The value = {}", v)), "Default value".to_string());
assert_eq!(we_have_it.map_or_else(|_| "Default value".to_string(), |v| format!("The value = {}", v)), "The value = 1".to_string());
assert_eq!(we_have_error.map_or_else(|_| "Default value".to_string(), |v| format!("The value = {}", v)), "Default value".to_string());
There are other important methods besides those methods in the std::result::Result documentation. Do check them because Option and Result are very important in Rust and Rocket.
Returning a string or numbers as an error might be acceptable in some cases, but most likely, we want a real error type with a message and possible backtrace that we can process further. In the next section, we are going to learn about (and use) the Error trait and return the dynamic error type in our application.
Rust has a trait to unify propagating errors by providing the std::error::Error trait. Since the Error trait is defined as pub trait Error: Debug + Display, any type that implements Error should also implement the Debug and Display traits.
Let's see how we can create a custom error type by creating a new module:
pub mod errors;
pub mod our_error;
use rocket::http::Status;
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub struct OurError {
pub status: Status,
pub message: String,
debug: Option<Box<dyn Error>>,
}
impl fmt::Display for OurError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) ->
fmt::Result {
write!(f, "{}", &self.message)
}
}
impl Error for OurError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
if self.debug.is_some() {
self.debug.as_ref().unwrap().source();
}
None
}
}
Currently, for the User module, we return a Result<..., Box<dyn Error>> dynamic error for each method. This is a common pattern of returning an error by using any type that implements Error and then putting the instance in the heap using Box.
The problem with this approach is we can only use methods provided by the Error trait, that is, source(). We want to be able to use the OurError status, message, and debug information.
impl OurError {
fn new_error_with_status(status: Status, message:
String, debug: Option<Box<dyn Error>>) -> Self {
OurError {
status,
message,
debug,
}
}
pub fn new_bad_request_error(message: String,
debug: Option<Box<dyn Error>>) -> Self {
Self::new_error_with_status(Status::
BadRequest, message, debug)
}
pub fn new_not_found_error(message: String,
debug: Option<Box<dyn Error>>) -> Self {
Self::new_error_with_status(Status::NotFound,
message, debug)
}
pub fn new_internal_server_error(
message: String,
debug: Option<Box<dyn Error>>,
) -> Self {
Self::new_error_with_status(Status::
InternalServerError, message, debug)
}
}
use sqlx::Error as sqlxError;
use uuid::Error as uuidError;
impl OurError {
...
pub fn from_uuid_error(e: uuidError) -> Self {
OurError::new_bad_request_error(
String::from("Something went wrong"),
Some(Box::new(e)))
}
}
use std::borrow::Cow;
....
impl OurError {
....
pub fn from_sqlx_error(e: sqlxError) -> Self {
match e {
sqlxError::RowNotFound => {
OurError::new_not_found_error(
String::from("Not found"),
Some(Box::new(e)))
}
sqlxError::Database(db) => {
if db.code().unwrap_or(Cow::
Borrowed("2300")).starts_with("23") {
return OurError::new_bad_
request_error(
String::from("Cannot create or
update resource"),
Some(Box::new(db)),
);
}
OurError::new_internal_server_error(
String::from("Something went
wrong"),
Some(Box::new(db)),
)
}
_ => OurError::new_internal_server_error(
String::from("Something went wrong"),
Some(Box::new(e)),
),
}
}
}
argon2 = {version = "0.3", features = ["std"]}
pub async fn find(connection: &mut PgConnection, uuid: &str) -> Result<Self, OurError> {
let parsed_uuid = Uuid::parse_str(
uuid).map_err(OurError::from_uuid_error)?;
let query_str = "SELECT * FROM users WHERE uuid =
$1";
Ok(sqlx::query_as::<_, Self>(query_str)
.bind(parsed_uuid)
.fetch_one(connection)
.await
.map_err(OurError::from_sqlx_error)?)
}
let password_hash = argon2
.hash_password(new_user.password.as_bytes(),
&salt)
.map_err(|e| {
OurError::new_internal_server_error(
String::from("Something went wrong"),
Some(Box::new(e)),
)
})?;
Change all the methods to use OurError. Just a reminder: you can find the complete source code for src/models/user.rs in the GitHub repository at https://github.com/PacktPublishing/Rust-Web-Development-with-Rocket/tree/main/Chapter07.
pub async fn get_user(...) -> HtmlResponse {
...
let user = User::find(connection,
uuid).await.map_err(|e| e.status)?;
...
}
...
pub async fn delete_user(...) -> Result<Flash<Redirect>, Flash<Redirect>> {
...
User::destroy(connection, uuid)
.await
.map_err(|e| Flash::error(Redirect::to("/
users"), format!("<div>{}</div>", e)))?;
...
}
You can find the complete source code for src/routes/user.rs in the GitHub repository. Now that we have implemented errors, it might be a good time to try to implement the catchers that we defined before in src/catchers/mod.rs to show default errors for the user. You can also see the example of the default catchers in the source code.
In an application, tracking and logging errors are an important part of maintaining the application. Since we implemented the Error trait, we can log the source() of an error in our application. Let's take a look at how to do that in the next section.
In Rust, there's a log crate that provides a facade for application logging. The log provides five macros: error!, warn!, info!, debug!, and trace!. An application can then create a log based on the severity and filter what needs to be logged, also based on the severity. For example, if we filter based on warn, then we only log error! and warn! and ignore the rest. Since the log crate does not implement the logging itself, people often use another crate to do the actual implementation. In the documentation for the log crate, we can find examples of other logging crates that can be used: env_logger, simple_logger, simplelog, pretty_env_logger, stderrlog, flexi_logger, log4rs, fern, syslog, and slog-stdlog.
Let's implement custom logging in our application. We will use the fern crate for logging and wrap that in async_log to make logging asynchronous:
async-log = "2.0.0"
fern = "0.6"
log = "0.4"
log_level = "normal"
fn setup_logger() {}
use log::LevelFilter;
...
let (level, logger) = fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"[{date}] [{level}][{target}] [{
message}]",
date = chrono::Local::now().format("[
%Y-%m-%d][%H:%M:%S%.3f]"),
target = record.target(),
level = record.level(),
message = message
))
})
.level(LevelFilter::Info)
.chain(std::io::stdout())
.chain(
fern::log_file("logs/application.log")
.unwrap_or_else(|_| panic!("Cannot open
logs/application.log")),
)
.into_log();
First, we create a new instance of fern::Dispatch. After that, we configure the output format using the format() method. After setting the output format, we set the log level using the level() method.
For the logger, we want to not only output the log to the operating system stdout, but we also want to write to a log file. We can do it using the chain() method. To avoid panicking, don't forget to create a logs folder in the application directory.
async_log::Logger::wrap(logger, || 0).start(level).unwrap();
impl OurError {
fn new_error_with_status(...) ... {
if debug.is_some() {
log::error!("Error: {:?}", &debug);
}
...
}
}
async fn rocket() -> Rocket<Build> {
setup_logger();
...
}
[[2021-11-21][17:50:49.366]] [ERROR][our_application::errors::our_error]
[Error: Some(PgDatabaseError { severity: Error, code: "23505", message:
"duplicate key value violates unique constraint "users_username_key""
, detail: Some("Key (username)=(karuna) already exists."), hint: None, p
osition: None, where: None, schema: Some("public"), table: Some("users")
, column: None, data_type: None, constraint: Some("users_username_key"),
file: Some("nbtinsert.c"), line: Some(649), routine: Some("_bt_check_un
ique") })]
Now that we have learned how to log errors, we can implement logging functionalities to improve the application. For example, we might want to create server-side analytics, or we can combine the logs with third-party monitoring as a service to improve the operations and create business intelligence.
In this chapter, we have learned some ways to handle errors in Rust and Rocket applications. We can use panic!, Option, and Result as a way to propagate errors and create handling for the errors.
We have also learned about creating a custom type that implements the Error trait. The type can store another error, creating an error chain.
Finally, we learned ways to log errors in our application. We can also use log capability to improve the application itself.
Our user pages are looking good, but using String all over the place is cumbersome, so in the next chapter, we are going to learn more about templating using CSS, JavaScript, and other assets in our application.
18.188.216.249