Oduah Chigozie Technical writer | Frontend developer | Blockchain developer

Using Rust with Axum for error handling

5 min read 1666

Using Rust With Axum For Error Handling

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:

Installing Axum

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.

Building a simple web server with Axum

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.

Understanding how the simple web server works

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 route
  • Router — for creating and handling routes

Underneath 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.

What is a handler?

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.

Handling errors in Rust with Axum

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:

Browser Open To Port 3000 In Light Mode With Http Error 500 Message Displayed

If we set a_condition to true, the handler returns Welcome! again, as it did previously.

Other methods of handling errors in Rust with Axum

Axum provides other ways to handle error responses besides Result::Err enums. Two of the major ways are fallible handlers and pattern matching.

Error handling with fallible handlers

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:

Browser Open To Port 3000 In Light Mode With Error Message Stating Something Went Wrong And Explaining What Went Wrong

Error handling with pattern matching

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:

Browser Open To Port 3000 In Light Mode Displaying Message Explaining Why Server Failed

Conclusion

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.

LogRocket: Full visibility into web frontends for Rust apps

Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking 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 app. 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 — .

Oduah Chigozie Technical writer | Frontend developer | Blockchain developer

Leave a Reply