When you’re building a backend web service, things don’t always go right. For example, an incoming request may not have the necessary data or may present its data in the wrong format. Most web services handle this possibility by returning error responses.
Axum is a framework for building server-side web applications using Rust. It also allows you to handle error responses.
This tutorial gives you a little refresher on building a web service with Rust and Axum, along with how to use this framework to handle error responses.
Jump ahead:
To set up an Axum project, the first thing you need to do is create an empty Rust project with this command:
$ cargo new --bin axum-web-server
The command above creates an axum-web-server
folder and initializes the files you need to get started.
The next thing you need to do is install the axum
and tokio
packages. To install them, open the Cargo.toml
file in the axum-web-server
folder and write the following under [dependencies]
:
axum = "0.6.1" tokio = { version = "1.23.0", features = ["full"] } tower = { version = "0.4.13", features = ["timeout"] }
Then build the package with the command below:
$ cargo build
As a reminder, when you’re building a project, cargo
downloads the packages you listed under [dependencies]
from its registry into your project.
With your project set up, it’s time to build a simple application. Later, we will use this simple application to demonstrate how errors are handled in Axum.
If you already have your project and just want to see Axum’s error-handling functionality in action, you can skip ahead.
In this section, the application you’ll build is a web service that has three endpoints:
/
— the root; returns the text Welcome
/say-hello
— returns the text Hello!
/say-goodbye
— returns the text Goodbye
The code for this server is below. Copy and paste the code into the src/main.rs
file in the axum-web-server
folder:
use axum::{ routing::get, Router, }; #[tokio::main] async fn main() { let app = Router::new() .route("/", get(root)) .route("/say-hello", get(say_hello)) .route("/say-goodbye", get(say_goodbye)); axum::Server::bind(&"127.0.0.1:3000".parse().unwrap()) .serve(app.into_make_service()) .await.unwrap(); } async fn root() -> String { return "Welcome!".to_string(); } async fn say_hello() -> String { return "Hello!".to_string(); } async fn say_goodbye() -> String { return "Goodbye!".to_string(); }
To see the code in action, run the following command:
$ cargo run
The command above runs the main.rs
file. It also starts a local web server at http://127.0.0.1:3000
while the program is running.
When you open http://127.0.0.1:3000
in your browser, you’ll get a message reading Welcome
on the browser page.
When you open http://127.0.0.1:3000/say-hello
instead, you’ll get a message reading Hello!
Opening a URL link in a browser is similar to sending a GET request to the URL. The browser displays the appropriate text response for the request in the browser page.
Now you may be wondering how the code works. The first thing the program does is to import the necessary packages:
use axum::{ routing::get, Router, };
The packages includes the following:
routing::get
— for registering GET request handlers to a routeRouter
— for creating and handling routesUnderneath the imports, the program defines the main function:
#[tokio::main] async fn main() { let app = Router::new() .route("/", get(root)) .route("/say-hello", get(say_hello)) .route("/say-goodbye", get(say_goodbye)); axum::Server::bind(&"127.0.0.1:3000".parse().unwrap()) .serve(app.into_make_service()) .await.unwrap(); }
By default, Rust doesn’t allow you to declare the main function as asynchronous, so you need to modify the function with the #[tokio::main]
attribute.
In the main
function, the first thing we did was create a router and register handlers for each route we want the web server to have:
let app = Router::new() .route("/", get(root)) .route("/say-hello/:name", get(say_hello)) .route("/say-goodbye/:name", get(say_goodbye));
The next thing we did was create a server, bind it to http://127.0.0.1:3000
, and register the router we created:
axum::Server::bind(&"127.0.0.1:3000".parse().unwrap()) .serve(app.into_make_service()) .await.unwrap();
Below the main
function, we declared the root
, say_hello
, and say_goodbye
handlers that we registered to the router:
async fn root() -> String { return "Welcome!".to_string(); } async fn say_hello() -> String { return "Hello!".to_string(); } async fn say_goodbye() -> String { return "Goodbye!".to_string(); }
Now, we can move on to error handling in Rust with Axum.
A handler is an asynchronous function that handles the logic behind a request.
Not only can the handler access the request data sent to the route, but it also provides the response to the request. Once you register a handler on a route, any request sent to the route will be dealt with by the handler.
The root
, say_hello
, and say_goodbye
functions in the earlier sections are simple examples of a handler:
async fn root() -> String { return "Welcome!".to_string(); } async fn say_hello() -> String { return "Hello!".to_string(); } async fn say_goodbye() -> String { return "Goodbye!".to_string(); }
At the end of a handler’s operation on a request, what the handler returns will be the server’s response.
For example, when you send a request to http://127.0.0.1:3000
, the program calls root
. Since root
returns Welcome!
, the root handler returns Welcome!
as its response.
Besides returning regular responses, you may need to return errors in a response. Errors are useful for letting the client know what went wrong with its request.
Axum requires all handlers to be infallible, meaning that they don’t run into errors while they’re running. That means the only way a handler can send error responses is to return a Result::Err enum
.
In the example below, we modify the root handler to include error handling capabilities:
async fn root() -> Result<String, StatusCode> { let a_condition = false; if a_condition { return Ok("Welcome!".to_string()); } else { return Err(StatusCode::INTERNAL_SERVER_ERROR); } }
As you can see, the handler returns an internal server error response because a_condition
is false.
With root
now modified, when you open http://127.0.0.1:3000/
in a browser, the web page will look like the image below:
If we set a_condition
to true, the handler returns Welcome!
again, as it did previously.
Axum provides other ways to handle error responses besides Result::Err
enums. Two of the major ways are fallible handlers and pattern matching.
You may not always know every error condition in a handler, and a handler can run into an error at runtime. However, Axum requires all handlers to be infallible. As a result, when the handler runs into an error, the incoming request won’t get a response.
A fallible handler returns an error response if the handler runs into an error. The following is an example of a fallible handler:
use axum::{ Router, http::{Response, StatusCode}, error_handling::HandleError, }; #[tokio::main] async fn main() { // this service might fail with `reqwest::Error` let fallible_service = tower::service_fn(|_req| async { let body = can_fail().await?; Ok::<_, reqwest::Error>(Response::new(body)) }); // Since fallible_service can fail with 'reqwest::Error', // you can't directly route it to "/". // Use route_service to convert any errors // encountered into a response let app = Router::new() .route_service("/", HandleError::new(fallible_service, handle_error) ); axum::Server::bind(&"127.0.0.1:3000".parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); } async fn can_fail() -> Result<String, reqwest::Error> { // send a request to a site that doesn't exist // so we can see the handler fail let body = reqwest::get("https://www.abcdth.org") .await? .text() .await?; Ok(body) } async fn handle_error(err: reqwest::Error) -> (StatusCode, String) { return ( StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", err), ); }
As you can see, the fallible_service
will fail and return an internal service error. You should then see a message that reads Something went wrong:
followed by an error message that explains what went wrong:
When an Axum server fails, it returns a no-content response. A server failure usually originates from statements that have the potential to fail.
To prevent the server from returning a no-content response, we need to handle the errors directly. A direct method of handling errors is through pattern matching.
To handle an error using pattern matching, simply wrap the statement where a potential error can occur with a match
block.
In the example below, the reqwest
statement is wrapped because it can fail:
use axum::{ Router, routing::get, http::StatusCode, }; #[tokio::main] async fn main() { let app = Router::new() .route("/", get(|| async { match reqwest::get("https://www.abcdth.org").await { Ok(res) => ( StatusCode::OK, res.text().await.unwrap(), ), Err(err) => ( StatusCode::INTERNAL_SERVER_ERROR, format!("Server failed with {}", err), ) } })); axum::Server::bind(&"127.0.0.1:3000".parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); }
As you can see, when the handler fails, it returns an internal service error, then displays a message that explains why it failed:
In this article, we looked at how to build a web service with Rust and Axum, along with how error handling works and the different ways you can handle error responses in Axum.
Axum is a powerful tool for building web applications and services. Learn more about the framework with the Rust docs on Axum, this article on how to deploy a Rust web server to Heroku, or the Rust docs on Axum status codes.
Debugging Rust applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking the performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Rust application. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Modernize how you debug your Rust apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowExplore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
Build a real-time image background remover in Vue using Transformers.js and WebGPU for client-side processing with privacy and efficiency.
Optimize search parameter handling in React and Next.js with nuqs for SEO-friendly, shareable URLs and a better user experience.