Rust Error Handling | How to Define Generic Error Types?

Result<T,E> is a common data type that allows propagating Rust errors without necessarily crashing the Rust program unless the program panics. The Result<T, E> is made out of T or generic data type when the result is Ok(T) or Err(E) whenever an error occurs. One common question new Rust developers ask is:

How do you define generic error types if a function could return Result<T, Error1> and Result<T, Error2>?

To define a generic error type, “box” the error types to generate a trait object Box<dyn std::error::Error> . Meaning, if you are using the Result<T, E> type, the Result with a generic error will be Result<T, Box<dyn std::error::Error>> . Below is an example of how to use it to define the output Result of a function:

fn myFunction() -> Result<(), Box<dyn std::error::Error> {
}

Why “boxing” the errors to define a generic error type

The Box struct has implementations capable of transforming data types using the Error trait to generate a Box<Error> trait object.

Having said that, if you are generating a custom struct, you must implement the Error trait if you want to “box” the errors in a generic error type such as Box<dyn std::error::Error> .

use std::error::Error;

#[derive(Debug)]
struct RandomStruct;

impl Error for RandomStruct {}

Example of using generic errors

You can find a more realistic example of using using a generic error type in the article explaining how to build a Rust API using Hyper which uses the following function to return a Response<Body> when triggering one of the API endpoints.

use rand::Rng;
use std::error::Error;
use hyper::body::Buf;
use hyper::{header, Body, Request, Response, StatusCode};

const INTERNAL_SERVER_ERROR: &str = "Internal Server Error";

async fn create_car(req: Request<Body>) -> Result<Response<Body>, Box<dyn Error + Send + Sync>> {
    // get the buffer from the request body
    let buffer = hyper::body::aggregate(req).await?;

    // add an id to the new_car
    let mut new_car: serde_json::Value = serde_json::from_reader(buffer.reader())?;

    let mut random = rand::thread_rng();

    let car_id: u8 = random.gen();
    new_car["id"] = serde_json::Value::from(car_id.to_string());

    let res = match serde_json::to_string(&new_car) {
        Ok(json) => Response::builder()
            .status(StatusCode::OK)
            .header(header::CONTENT_TYPE, "application/json")
            .body(Body::from(json))
            .unwrap(),
        Err(_) => Response::builder()
            .status(StatusCode::INTERNAL_SERVER_ERROR)
            .body(INTERNAL_SERVER_ERROR.into())
            .unwrap(),
    };

    Ok(res)
}

Here is a quick summary of what is happening in the create_car function above:

  • First, the create_car function reads the client’s request body as a buffer.
  • Then, the buffer is deserialized to generate a JSON value serde_json::Value .
  • Then, it generates a new id to the JSON value.
  • Finally, it returns a Response<body> .

While the code handles potential errors from serializing a JSON value into a string in the end of the function (serde_json::to_string(&new_car) ), there are a couple of lines of code where it could generate errors:

  1. let buffer = hyper::body::aggregate(req).await?
  2. let mut new_car: serde_json::Value = serde_json::from_reader(buffer.reader())?

These two lines return the following errors respectively:

  1. hyper::Error
  2. serde_json::Error>

One solution is to replace the ? operator with a match pattern and properly handle all cases to return a common error type. This can make the logic of the create_car function verbose.

Another option is to generate a custom error type. However, this quickly adds more work to handle errors that might not be needed at all.

Finally and the approach used in the create_car function is to keep the ? operator and return a generic error type. In this case, the Result type is Result<Response<Body>, Box<dyn Error + Send + Sync>>.

Technically it would ok to define the result type Result<Response<Body>, Box<dyn Error>> for the create_car function. However, if you follow the article, you will find out a caller function calling the create_car function could generate errors that cannot be automatically converted to an error std::error::Error.

use rand::Rng;
use std::net::SocketAddr;
use std::error::Error;
use hyper::body::Buf;
use hyper::server::conn::Http;
use hyper::service::service_fn;
use hyper::{header, Body, Method, Request, Response, StatusCode};
use serde::{Deserialize, Serialize};
use tokio::net::TcpListener;

const INTERNAL_SERVER_ERROR: &str = "Internal Server Error";

async fn create_car(req: Request<Body>) -> Result<Response<Body>, Box<dyn Error>> {
    // get the buffer from the request body
    let buffer = hyper::body::aggregate(req).await?;

    // add an id to the new_car
    let mut new_car: serde_json::Value = serde_json::from_reader(buffer.reader())?;

    let mut random = rand::thread_rng();

    let car_id: u8 = random.gen();
    new_car["id"] = serde_json::Value::from(car_id.to_string());
    
    let res = match serde_json::to_string(&new_car) {
        Ok(json) => Response::builder()
            .status(StatusCode::OK)
            .header(header::CONTENT_TYPE, "application/json")
            .body(Body::from(json))
            .unwrap(),
        Err(_) => Response::builder()
            .status(StatusCode::INTERNAL_SERVER_ERROR)
            .body(INTERNAL_SERVER_ERROR.into())
            .unwrap(),
    };

    Ok(res)
}

async fn cars_handler(req: Request<Body>) -> Result<Response<Body>, Box<dyn Error>> {
    let path = req.uri().path().to_owned();
    let path_segments = path.split("/").collect::<Vec<&str>>();
    let base_path = path_segments[1];

    match (req.method(), base_path) {
        (&Method::POST, "cars") => create_car(req).await,

        // Return the 404 Not Found for other routes.
        _ => {
            let mut not_found = Response::default();
            *not_found.status_mut() = StatusCode::NOT_FOUND;
            Ok(not_found)
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let listener = TcpListener::bind(addr).await?;
    println!("Listening on http://{}", addr);
    loop {
        let (stream, _) = listener.accept().await?;

        tokio::task::spawn(async move {
            if let Err(err) = Http::new()
                .serve_connection(stream, service_fn(cars_handler))
                .await
            {
                println!("Error serving connection: {:?}", err);
            }
        });
    }
}

In this case, the cars_handler function is the caller function of create_car function. Also, the main function is the caller function of the cars_handler function. If you noticed, the main function can return a different type of error in the following lines of code:

  1. let listener = TcpListener::bind(addr).await?;
  2. let (stream, _) = listener.accept().await?;

For this specific scenario, the main function defines the error type Box<dyn Error + Send + Sync> as the traits Send and Sync include implementations to properly transform special errors returned in the previous two lines of code. In other words, the Box<dyn std::error::Error> cannot transform the TcpListener related-errors.

Problem of using a generic error data type

The main problem of using generic error types is to not able to detect errors of a specific type during compilation. This means these errors can only be detected during runtime.

Conclusion

All in all, “Box” the errors if you need to define a generic error. In most cases, the Box<dyn std::error::Error> will work to create a generic error. Generic errors are a good way to keep your code simple and without the need of adding more boilerplate such as using match patterns to return a common error type, or generating a custom error type which can quickly add a few lines of code to properly handle errors.

Did this article help?

Let me know if this article was helpful by sharing your comments and tagging the Twitter account of Become A Better Programmer or to my personal Twitter account.