How to design a clean Async Router Trait in Rust without complex closures and downcasting hacks?

2026-08-23

Stack Overflow: View Question

Tags: asynchronous, rust, traits

Score: -1 | Views: 49

The asker wants a HashMap of route paths to async handlers. They've fallen into the classic pit: async fn in a trait desugars to impl Future, which isn't object-safe in the naive form, so they've reached for dyn_clone, Any::downcast_mut, and hand-rolled Pin<Box<dyn Future>> to make everything fit behind a dyn pointer.

Why it's genuinely hard: Rust's async model is zero-cost — each async fn produces a unique anonymous Future type. Erasing that into a uniform handler type requires boxing the future somewhere. The question is where to hide the box so callers don't see it. Meanwhile, handlers have different argument shapes (path params, query, JSON body, state), so a "one signature fits all" trait tends to devolve into Box<dyn Any> plumbing — which is exactly what the asker is trying to escape.

The clean approach mainstream frameworks use (axum, actix) is the extractor pattern combined with a blanket-impl Handler trait:

trait Handler<Args>: Clone + Send + 'static {
    type Future: Future<Output = Response> + Send;
    fn call(self, req: Request) -> Self::Future;
}

// Blanket impl for every async fn shape you support
impl<F, Fut, A1> Handler<(A1,)> for F
where
    F: Fn(A1) -> Fut + Clone + Send + 'static,
    Fut: Future<Output = Response> + Send,
    A1: FromRequest,
{ /* extract A1, call self, return Fut */ }

Then wrap concrete handlers in a small erased type at registration time:

type BoxedHandler = Arc<dyn Fn(Request) -> BoxFuture<'static, Response> + Send + Sync>;

fn erase<H, A>(h: H) -> BoxedHandler
where H: Handler<A>, A: FromRequest + 'static
{
    Arc::new(move |req| Box::pin(h.clone().call(req)))
}

The HashMap<String, BoxedHandler> then holds uniform values. No Any, no dyn_clone, no downcasting — the type erasure happens exactly once at insertion, and the boxing cost is one allocation per request (unavoidable for dynamic dispatch).

Gotchas:

The challenge: Cleanly erasing heterogeneous async handler types into a uniform map value requires shifting the abstraction from "trait object of futures" to "blanket-impl handler trait + one-shot boxing at registration," which is exactly the architectural move every mature Rust web framework has independently converged on.

All newsletters