Simplifying Asynchronous Code in Express with asyncHandler (With TypeScript)

Handling asynchronous route handlers in Express can be tedious and error-prone, but using an asyncHandler
utility makes it significantly cleaner and more maintainable. This article explores how to implement asyncHandler
with TypeScript to ensure type safety while avoiding repetitive boilerplate.
Key Takeaway:
asyncHandler
helps you simplify asynchronous code in Express by automatically catching and forwarding errors to the error-handling middleware.
The Problem with Asynchronous Handlers
Using async/await
in route handlers often requires wrapping code in try-catch
blocks to manage errors manually. Here’s an example:
import { Request, Response, NextFunction } from "express";
app.get("/example", async (req: Request, res: Response, next: NextFunction) => {
try {
const data = await someAsyncOperation();
res.json(data);
} catch (error) {
next(error); // Forward the error to the error-handling middleware
}
});
While this works, the repeated try-catch
pattern quickly becomes boilerplate that clutters your codebase.
Introducing asyncHandler
The asyncHandler
utility wraps asynchronous route handlers, catching any errors and forwarding them to Express's error-handling middleware. This eliminates the need for repetitive try-catch
blocks.
Implementation
Here’s a type-safe implementation of asyncHandler
in TypeScript:
import { Request, Response, NextFunction } from "express";
type AsyncHandlerFunction<T = void> = (
req: Request,
res: Response,
next: NextFunction,
) => Promise<T>;
const asyncHandler = <T = void>(fn: AsyncHandlerFunction<T>) => {
return (req: Request, res: Response, next: NextFunction): void => {
fn(req, res, next).catch(next);
};
};
export default asyncHandler;
Key Features:
- Generic Return Type: The
<T>
generic ensures flexibility in the handler’s return type, improving type safety. - Strong Typing: The
AsyncHandlerFunction
type enforces compatibility with Express types (Request
,Response
,NextFunction
). - Error Propagation: The
.catch(next)
forwards all errors to the middleware consistently.
Using asyncHandler
in an Express Application
Here’s how to use asyncHandler
to clean up your asynchronous route handlers:
Without asyncHandler
app.get("/users", async (req: Request, res: Response, next: NextFunction) => {
try {
const users = await getUsersFromDatabase();
res.json(users);
} catch (error) {
next(error);
}
});
With asyncHandler
import asyncHandler from "./asyncHandler";
app.get(
"/users",
asyncHandler(async (req: Request, res: Response) => {
const users = await getUsersFromDatabase();
res.json(users);
}),
);
Adding Error-Handling Middleware
For asyncHandler
to work correctly, you need error-handling middleware in your Express app:
import { Request, Response, NextFunction } from "express";
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: err.message || "Internal Server Error" });
});
The Error
type ensures only valid error objects are passed to the middleware.
Full Application Example
Here’s a complete example demonstrating asyncHandler
in action:
import express, { Request, Response, NextFunction } from "express";
import asyncHandler from "./asyncHandler";
const app = express();
// Mock database operation
const getUsersFromDatabase = async (): Promise<
{ id: number; name: string }[]
> => {
return [
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Smith" },
];
};
// Asynchronous route using asyncHandler
app.get(
"/users",
asyncHandler(async (req: Request, res: Response) => {
const users = await getUsersFromDatabase();
res.json(users);
}),
);
// Error-handling middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: err.message || "Internal Server Error" });
});
// Start the server
app.listen(3000, () => {
console.log("Server is running on http://localhost:3000");
});
Features:
- Type Safety: Strongly typed route handlers and database functions.
- Cleaner Code: No repetitive
try-catch
blocks. - Centralized Error Handling: All errors are consistently forwarded.
Summary
The asyncHandler
utility simplifies error handling in Express, allowing you to write clean and maintainable asynchronous route handlers. By integrating it with TypeScript:
- Type Safety: Robust type definitions for handlers and middleware.
- Code Cleanliness: Elimination of repetitive boilerplate.
- Error Consistency: Centralized error management.
Adopting asyncHandler
—either by implementing it yourself or using a library like express-async-handler—is a best practice for modern Express applications.