Handlers

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against axum 0.8.9 on Rust 1.99 (edition 2024) with tokio 1.53, checked on 2026-10-06. A handler is the async function that runs for a matched route. axum’s whole developer experience rests on one trick: any function whose arguments are extractors and whose output is a response type implements the Handler trait automatically. When the trick fails, the compiler errors are long; this page explains the rules so you can read them.

Where it runs in the request

In the request lifecycle (tokio accepts the connection, hyper parses the request, tower layers wrap the router, the router matches a route, middleware and extractors run, the handler runs, the response travels back out), the handler is the innermost step: it runs after routing, after every middleware layer and after all its extractors have succeeded, and its return value starts the response’s way back out through the same layers. Everything on this page — what makes a function a handler, the Send bound, #[debug_handler] — is about that one call.

What makes a function a handler

use axum::{
    Json, Router,
    extract::{Path, Query, State},
    http::StatusCode,
    routing::get,
};
use serde::{Deserialize, Serialize};

#[derive(Clone)]
struct AppState {
    library_name: &'static str,
}

#[derive(Deserialize)]
struct ReviewQuery {
    #[serde(default)]
    page: u32,
}

#[derive(Serialize)]
struct ReviewPage {
    library: &'static str,
    isbn: String,
    page: u32,
    reviews: Vec<String>,
}

// three extractors in, a Result out
async fn list_reviews(
    State(state): State<AppState>,
    Path(isbn): Path<String>,
    Query(q): Query<ReviewQuery>,
) -> Result<Json<ReviewPage>, StatusCode> {
    if isbn.len() != 13 {
        return Err(StatusCode::BAD_REQUEST);
    }
    Ok(Json(ReviewPage { library: state.library_name, isbn, page: q.page, reviews: vec!["A classic.".into()] }))
}

fn app() -> Router {
    Router::new()
        .route("/api/v1/books/{isbn}/reviews", get(list_reviews))
        .with_state(AppState { library_name: "Bookshelf" })
}

#[tokio::main]
async fn main() {
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app()).await.unwrap();
}

Source: axum: handler

A function is a handler when all of the following hold:

  1. It is an async fn (or a function/closure returning a future).

  2. It takes zero to 16 arguments. Every argument except the last must implement FromRequestParts (it reads only the request line and headers); the last may instead implement FromRequest, which can consume the body. See Extractors.

  3. Its output implements IntoResponse (see Responses).

  4. The returned future is Send, because tokio’s multi-thread scheduler may resume it on another worker thread.

  5. The function itself is Clone + Send + Sync + 'static. The Sync requirement is new in axum 0.8; plain functions and closures that capture Arcs satisfy it, closures capturing a Cell or Rc do not.

axum implements Handler<T, S> for every function type that satisfies these bounds through a macro covering 0 to 16 arguments. The type parameter T is the tuple of extractor types (inferred), and S is the state type the handler needs. The trait itself has one required method, call(self, req, state), which you never call directly.

Source: axum: Handler

Closures as handlers

Closures work wherever functions do, which is convenient for one-liners and for capturing configuration:

use std::sync::Arc;

use axum::{Router, extract::Path, routing::get};

#[tokio::main]
async fn main() {
    let greeting = Arc::new(String::from("Welcome to Bookshelf"));

    let app = Router::new()
        .route("/", get(|| async { "home" }))
        .route(
            "/hello/{name}",
            get({
                let greeting = Arc::clone(&greeting);
                move |Path(name): Path<String>| async move { format!("{greeting}, {name}!") }
            }),
        );

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

The closure is cloned for every request, so capture Arcs rather than large values, and prefer State once more than one handler needs the same data (see State & Dependency Injection). The move ... async move pair is needed because the closure must own its captures ('static) and the async block must own its copy of them.

#[debug_handler]

When a function fails one of the rules, the error appears where it is passed to get/post and mentions only the Handler trait:

error[E0277]: the trait bound `fn(State<AppState>) -> impl Future<Output = &'static str> {lend}: Handler<_, _>`
is not satisfied
   --> src/routes/loans.rs:18:59
    |
 18 |     Router::new().route("/loans", post(lend))
    |                                   ---- ^^^^ the trait `Handler<_, _>` is not implemented for fn item ...
    |
    = note: Consider using `#[axum::debug_handler]` to improve the error message

#[axum::debug_handler] (axum’s macros feature) expands into separate compile-time checks for each argument, the argument order, the return type and the future’s Send-ness, so the error points at the real cause. It accepts state = AppState when the state type cannot be inferred from the arguments (for example when only a substate is extracted through FromRef). It has no effect on behaviour or performance and only works on functions, not closures.

Message with #[debug_handler] Cause and fix

the trait bound `+u32: FromRequest<...> is not satisfied+` / "Function argument is not a valid axum extractor"

an argument type is not an extractor; wrap it (Path<u32>, Query<T>, State<S>)

"`Json<_>` consumes the request body and thus must be the last argument to the handler function"

a body extractor (Json, Form, String, Bytes, Request, Multipart) is not last; move it

the trait bound `X: IntoResponse` is not satisfied

the return type is not a response; return Json<X>, implement IntoResponse for X, or map errors into an AppError

"future cannot be sent between threads safely"

a non-Send value is alive across an .await (below)

"Handlers cannot take more than 16 arguments"

nest extractors in a tuple ((a, b): (ExtractorA, ExtractorB)) or group them in a struct with #[derive(FromRequestParts)]

mismatched types: expected State<AppState>, found State<OtherState>

the router’s state type differs from the handler’s; see the state page

The "future is not `Send`" pitfall

The most common handler error in real services comes from holding a std::sync::MutexGuard — or an Rc, a RefCell borrow, or any other non-Send value — while awaiting:

use std::sync::{Arc, Mutex};

use axum::{Router, extract::State, routing::post};

#[derive(Clone, Default)]
struct AppState {
    loans: Arc<Mutex<Vec<String>>>,
}

async fn notify(_member: &str) {}

#[axum::debug_handler]
async fn lend(State(state): State<AppState>) -> &'static str {
    let mut loans = state.loans.lock().unwrap();
    loans.push("Dune".into());
    notify("member@example.com").await; // the guard is still alive here
    "lent"
}

fn main() {
    let _app: Router = Router::new().route("/loans", post(lend)).with_state(AppState::default());
    // error: future cannot be sent between threads safely
    //   = help: within `impl Future<Output = &'static str>`, the trait `Send` is not implemented for
    //           `std::sync::MutexGuard<'_, Vec<String>>`
    // error[E0277]: the trait bound `fn(State<AppState>) -> ... {lend}: Handler<_, _>` is not satisfied
}

A MutexGuard must be released on the thread that acquired it, so it is not Send; if the task were moved to another worker while suspended at the .await, that rule would break. The compiler therefore refuses to make the future Send. The fix is almost never a tokio::sync::Mutex; it is to end the critical section before awaiting:

use std::sync::{Arc, Mutex};

use axum::{Router, extract::State, routing::post};

#[derive(Clone, Default)]
struct AppState {
    loans: Arc<Mutex<Vec<String>>>,
}

async fn notify(_member: &str) {}

async fn lend(State(state): State<AppState>) -> &'static str {
    {
        let mut loans = state.loans.lock().expect("loans lock poisoned");
        loans.push("Dune".into());
    } // guard dropped here, before any .await
    notify("member@example.com").await;
    "lent"
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/loans", post(lend)).with_state(AppState::default());
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

An inner block, a helper function that does the locked work synchronously, or an explicit drop(guard) all work (the block is the most robust: drop alone does not always convince the compiler in older code). Use tokio::sync::Mutex only when the lock must genuinely be held across an .await, such as serialising access to a single connection. The Shared State, Send/Sync & Atomics page covers Send and Sync in general.

Handlers with and without state

A handler that extracts State<S> needs a router that has been given an S. Router::with_state supplies it for every route; a single handler can be turned into a self-contained service with Handler::with_state(state), which returns a HandlerService usable with route_service or nest_service.

A handler that needs no state can be used as a service directly through HandlerWithoutStateExt:

use axum::{
    Router,
    handler::HandlerWithoutStateExt,
    http::StatusCode,
    response::IntoResponse,
};
use tower_http::services::ServeDir;

async fn missing_asset() -> impl IntoResponse {
    (StatusCode::NOT_FOUND, "no such file")
}

#[tokio::main]
async fn main() {
    // ServeDir's fallback must be a Service; `into_service` adapts the handler.
    let static_files = ServeDir::new("static").not_found_service(missing_asset.into_service());
    let app = Router::new().nest_service("/static", static_files);

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

HandlerWithoutStateExt also offers into_make_service and into_make_service_with_connect_info, so a single handler can be served without a router at all — axum::serve(listener, handler.into_make_service()) — which is handy for a one-endpoint redirect server (see TLS & HTTPS).

Keep handlers thin

A handler’s job is translation: extract and validate input, call a service function, map the result to a response. Business rules belong in plain functions that know nothing about axum, so they can be unit-tested without a router and reused by background jobs and CLIs (see Project Structure). A handler longer than a screen is usually doing a service’s work.

Where the books are outdated

Yoon (Beginning Axum, chapter 3) writes handlers with unwrap() on database results and request data. A panic in a handler does not crash the server — it unwinds only the task serving that connection — but the client gets a dropped connection instead of a response, and nothing useful reaches the logs unless a panic hook or CatchPanicLayer is installed. Return Result<T, AppError> and use ? instead, keeping expect for start-up code (see Error Handling). axum 0.7 handlers did not need to be Sync; a 0.7 handler that captures a non-Sync value in a closure fails to compile on 0.8.