Roy Lopez
PersistDev.blog
#javascript

Simplifying Asynchronous Code in Express with asyncHandler (With TypeScript)

Simplifying Asynchronous Code in Express with asyncHandler (With TypeScript)
0 views
4 min read
#javascript

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:

  1. Generic Return Type: The <T> generic ensures flexibility in the handler’s return type, improving type safety.
  2. Strong Typing: The AsyncHandlerFunction type enforces compatibility with Express types (Request, Response, NextFunction).
  3. 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.

Loading...