Strona główna / Artykuły

Usunięcie standardowego kodu try/catch z tras Express za pomocą asyncHandler

Dowiedz się, jak middleware obsługujący błędy, obudowa asyncHandler oraz niestandardowa klasa AppError przenoszą odpowiedzi na błędy w Express z każdej trasy do jednego spójnego miejsca.

Wydawca: REACTAPP.TOP5 min czytania1231 słów

Źródła: Stop Repeating try/catch: A Cleaner Error Handling Pattern in Express.js

Większość Express API rozpoczyna się od bloku try/catch w każdej asynchronicznej ścieżce, który przekształca błędy w odpowiedź JSON. W miarę rozwoju API ten blok jest kopiowany do każdego obsługiwacza. Tutaj przeniesiemy odpowiedzi na błędy do jednego middleware, usuniemy powtarzający się blok try/catch za pomocą małego otocznika oraz pokażemy kody stanu za pomocą specjalnej klasy błędu. W przypadku problemów na poziomie procesu zapoznaj się z naszym przewodnikiem dotyczącym obsługi błędów na poziomie produkcyjnym w aplikacjach Node.js.

Dlaczego podejście try/catch dla każdej ścieżki nie jest skalowalne

Pierwsza wersja zazwyczaj otacza logikę i wysyła kod 500 w przypadku błędu.

app.get("/users", async (req, res) => {
  try {
    const users = await getUsers();
    res.json(users);
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

Tutaj nie ma żadnego problemu, dopóki ta sama struktura nie pojawia się w dziesiątkach ścieżek.

app.get("/users", async (req, res) => {
  try {
    // ...
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

app.get("/products", async (req, res) => {
  try {
    // ...
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

app.post("/orders", async (req, res) => {
  try {
    // ...
  } catch (err) {
    res.status(500).json({ message: err.message });
  }
});

Każdy obsługiwacz teraz wykonywa dwa zadania: logikę biznesową oraz określanie wyglądu błędów w transmisji danych. Zmiana formatu oznacza konieczność edytowania każdej trasy.

Middleware do obsługi błędów w Express

Express rozpoznaje middleware do obsługi błędów po liczbie parametrów: funkcja z czterema parametrami, przy czym błąd znajduje się na początku.

(err, req, res, next)

Wersja minimalna rejestruje błąd i zwraca ogólny kod 500. Należy ją zarejestrować po trasach, ponieważ Express uruchamia middleware w określonej kolejności. Zachowaj wszystkie cztery parametry, nawet jeśli next nie jest używany – w przeciwnym razie Express potraktuje to jako zwykły middleware.

app.use((err, req, res, next) => {
  console.error(err);
  res.status(500).json({
    message: "Internal Server Error"
  });
});

Przekierowywanie błędów za pomocą next(err)

Zamiast budować odpowiedź wewnątrz bloku catch, trasa przekazuje błąd do Express.

app.get("/users", async (req, res, next) => {
  try {
    const users = await getUsers();
    res.json(users);
  } catch (err) {
    next(err);
  }
});

next(err) sprawia, że Express pomija zwykły middleware i przechodzi bezpośrednio do obsługiwacza błędów:

Request
   ↓
Route
   ↓
Business logic
   ↓
Error?
   ↓
next(err)
   ↓
Centralized error handler
   ↓
HTTP Response

Usuwanie pozostałych bloków try/catch

Każda trasa nadal zawiera blok try/catch. Funkcja asyncHandler otacza funkcję trasy, uruchamia ją za pomocą Promise.resolve i przekazuje wszelkie odmowy do funkcji next.

const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

Trasa sprowadza się do swojej rzeczywistej logiki:

app.get(
  "/users",
  asyncHandler(async (req, res) => {
    const users = await getUsers();
    res.json(users);
  })
);

Gdy getUsers() zwraca odmowę, funkcja otaczająca wywołuje:

next(err);

W Express 5 odmowy asynchroniczne są automatycznie przekazywane do next, więc funkcja otaczająca ma znaczenie głównie w Express 4.

Wysyłanie kodów stanu za pomocą AppError

Błędy stają się bardziej przydatne, gdy zawierają kod stanu HTTP:

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
  }
}

Trasy rzucają sensowne błędy zamiast formatować odpowiedzi:

app.get(
  "/users/:id",
  asyncHandler(async (req, res) => {
    const user = await getUser(req.params.id);
    if (!user) {
      throw new AppError("User not found", 404);
    }
    res.json(user);
  })
);

Obsługa odczytuje statusCode, przechodząc w razie potrzeby na wartość 500:

app.use((err, req, res, next) => {
  console.error(err);
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({
    success: false,
    message: err.message || "Internal Server Error"
  });
});

Brakujący użytkownik teraz powoduje błąd 404 z następującym komunikatem:

{
  "success": false,
  "message": "User not found"
}

Uwaga: wyświetlanie err.message dla każdego błędu może ujawnić informacje wewnętrzne, takie jak komunikaty z bazy danych. Zwracaj go tylko w przypadku instancji AppError, a w pozostałych sytuacjach zwykły tekst. W przypadku standardowej struktury danych patrz nasz przegląd szczegółów problemu RFC 9457.

Pełna struktura projektu

Prosta struktura:

src/
├── controllers/
│   └── user.controller.js
├── middleware/
│   ├── asyncHandler.js
│   └── errorHandler.js
├── errors/
│   └── AppError.js
├── routes/
│   └── user.routes.js
└── app.js

Moduł otulacz

const asyncHandler = (fn) => {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
};

module.exports = asyncHandler;

Moduł klasy błędu

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
  }
}

module.exports = AppError;

Moduł obsługujący błędy

const errorHandler = (err, req, res, next) => {
  console.error(err);
  res.status(err.statusCode || 500).json({
    success: false,
    message: err.message || "Internal Server Error"
  });
};
module.exports = errorHandler;

Kontroler wykorzystujący oba elementy

Kontrolery pobierają dane, w razie potrzeby rzucają AppError, a następnie zwracają wynik sukcesu.

const asyncHandler = require("../middleware/asyncHandler");
const AppError = require("../errors/AppError");

const getUsers = asyncHandler(async (req, res) => {
  const users = await userService.getUsers();
  res.json({
    success: true,
    data: users
  });
});

const getUser = asyncHandler(async (req, res) => {
  const user = await userService.getUser(req.params.id);
  if (!user) {
    throw new AppError("User not found", 404);
  }
  res.json({
    success: true,
    data: user
  });
});

module.exports = {
  getUsers,
  getUser
};

Podłączanie w pliku app.js

Obsługa błędów jest umieszczana na końcu.

const express = require("express");
const userRoutes = require("./routes/user.routes");
const errorHandler = require("./middleware/errorHandler");

const app = express();
app.use(express.json());

app.use("/users", userRoutes);

// Must be after routes
app.use(errorHandler);

module.exports = app;

Co daje ten wzorzec

Mniej powtarzającego się kodu

Kontrolery przestają się powtarzać:

try {
  // ...
} catch (err) {
  res.status(500).json(...);
}

Jeden format błędów

Klienci mogą polegać na jednej strukturze:

{
  "success": false,
  "message": "User not found"
}

Kontrolery skupione na jednym celu

Zadanie kontrolera staje się prostsze:

Get data
↓
Validate
↓
Process
↓
Return result

Jedno miejsce do monitorowania

Logowanie, identyfikatory zapytań i śledzenie błędów znajdują się w jednym middleware.

Kolejność ma znaczenie: zarejestruj handler na końcu

Musi następować po każdym routerze, który obsługuje:

app.use("/users", userRoutes);
app.use("/products", productRoutes);

// Error handler LAST
app.use(errorHandler);

Zarejestrowany wcześniej nie widzi błędów z późniejszych tras.

Podsumowanie

Zamiast:

Route
 └── try/catch
      └── send error response

staje się:

Route
 └── business logic
      └── throw error
           ↓
      asyncHandler
           ↓
      centralized error handler
           ↓
      consistent HTTP response

Prawdziwym celem jest oddzielenie logiki biznesowej od prezentacji błędów:

  • Middleware do obsługi błędów wymaga czterech parametrów i musi być zarejestrowany na końcu.
  • Użyj asyncHandler w Express 4; Express 5 sam obsługuje odrzucenia asynchroniczne.
  • Rzuć AppError wraz z kodem stanu dla oczekiwanych błędów i ukryj komunikaty dotyczące nieoczekiwanych.
  • Literatura pokrewna