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.
Ź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.
asyncHandler w Express 4; Express 5 sam obsługuje odrzucenia asynchroniczne.AppError wraz z kodem stanu dla oczekiwanych błędów i ukryj komunikaty dotyczące nieoczekiwanych.Literatura pokrewna
- Balansowanie generacji Prisma CRUD z intencjonalną kontrolą tras — Dowiedz się, jak generowanie routerów Prisma CRUD oparte na schemacie może eliminować powtarzalne fragmenty kodu, zachowując jednocześnie decyzje dotyczące zaufania, zakresu i dostępu w kodzie aplikacji.
- Projektowanie warstwowe API w Node.js: od rozbudowanych kontrolerów do czystej architektury — Dowiedz się, jak przeredagować API w Node.js na warstwy kontrolerów, usług i dostępu do danych, aby rozwiązać problemy z skomplikowaną logiką biznesową, niejednolitymi błędami oraz trudnościami w skalowaniu.
- Wysyłanie i odbieranie SMS w Node.js za pomocą Twilio Webhooks i Express — Stwórz mały serwer Express, który wysyła wiadomości przez Twilio, odbiera odpowiedzi za pośrednictwem webhooku udostępnionego przez ngrok i automatycznie odpowiada na nie za pomocą TwiML.
- Obcinanie stringów bez uszkodzenia emoji w JavaScript z pomocą Intl.Segmenter — Dlaczego metody slice() i obcinanie oparte na rozszerzaniu uszkadzają emoji oraz tekst z akcentami, jak to wpłynęło na działanie rzeczywistych produktów oraz jak zamiast tego obcinać stringi na granicach grafemów.
- Walidacja danych po stronie serwera w Express z wykorzystaniem łańcuchów express-validator — Dowiedz się, dlaczego API Express muszą walidować dane na serwerze, jak łańcuchy express-validator rejestrują błędy dla validationResult oraz jak skonstruować trasę produkcyjną.