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> {
}
Table of Contents
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:
let buffer = hyper::body::aggregate(req).await?
let mut new_car: serde_json::Value = serde_json::from_reader(buffer.reader())?
These two lines return the following errors respectively:
hyper::Error
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:
let listener = TcpListener::bind(addr).await?;
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.