Головна / Статті

Видалення шаблонного коду try/catch з маршрутів Express за допомогою asyncHandler

Дізнайтеся, як мідлвейр для обробки помилок, обгортка asyncHandler та власний клас AppError дозволяють перемістити відповіді Express щодо помилок з кожного маршруту в одне уніфіковане місце.

Видавець: REACTAPP.TOP5 хв читання1231 слів

Джерела: Stop Repeating try/catch: A Cleaner Error Handling Pattern in Express.js

Більшість Express API починаються з блоку try/catch у кожному асинхронному маршруті, який перетворює помилки на відповідь у форматі JSON. У міру зростання API цей блок копіюється до кожного обробника. Тут ви перемістите відповіді на помилки до одного проміжку, приберете повторюваний блок try/catch за допомогою невеликого обгортки та використаєте спеціальний клас помилок для визначення кодів статусу. Щодо помилок на рівні процесу, дивіться наш посібник щодо обробки помилок промислового рівня в Node.js.

Чому підхід try/catch для кожного маршруту не є масштабованим

Перша версія зазвичай обгортає логіку та надсилає код 500 у разі помилки.

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

Тут немає жодних проблем, поки ця структура не починає повторюватися у десятках маршрутів.

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 });
  }
});

Кожен обробник тепер виконує дві функції: обробку бізнес-логіки та визначення формату повідомлень про помилки під час передачі даних. Зміна формату означає необхідність редагування кожної маршрутної таблиці.

Проміжний код для обробки помилок у Express

Express розпізнає проміжний код для обробки помилок за кількістю параметрів: функція з чотирма параметрами, де першим йде інформація про помилку.

(err, req, res, next)

Мінімальна версія цього проміжного коду фіксує помилку та повертає загальне повідомлення 500. Його потрібно зареєструвати після ваших маршрутів, оскільки Express виконує проміжні коди у певному порядку. Зберігайте всі чотири параметри, навіть якщо функція next не використовується, адже Express буде розглядати її як звичайний проміжний код.

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

Пересилання помилок за допомогою next(err)

Замість створення відповіді всередині блоку catch, маршрут передає помилку до Express.

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

Функція next(err) змушує Express пропустити звичайні проміжні коди та перейти до обробника помилок:

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

Видалення залишкових try/catch

Кожна маршрутна функція все ще містить блоки try/catch. Функція asyncHandler обгортає маршрутну функцію, виконує її через Promise.resolve та надсилає будь-які помилки до функції next.

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

Маршрут скорочується до своєї справжньої логіки:

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

Коли функція getUsers() повертає помилку, обгортка викликає:

next(err);

У Express 5 асинхронні помилки автоматично передаються до next, тому обгортка має значення переважно у Express 4.

Позначення кодів стану за допомогою AppError

Помилки стають кориснішими, коли вони містять код стану HTTP:

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

Маршрути кидають змістовні помилки замість форматування відповідей:

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);
  })
);

Обробник читає значення statusCode, при цьому якщо воно відсутнє, використовується код 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"
  });
});

Відсутній користувач тепер призводить до коду 404 з наступним повідомленням:

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

Увага: відтворення err.message для кожної помилки може вивести надані дані, такі як повідомлення бази даних. Повертайте його лише для екземплярів AppError, а в інших випадках — загальний текст. Щодо стандартної структури даних, дивіться наш огляд деталей проблеми RFC 9457.

Повна структура проекту

Проста структура:

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

Модуль-обгортка

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

module.exports = asyncHandler;

Модуль класу помилки

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

module.exports = AppError;

Модуль обробника помилок

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;

Контролер, який використовує обидва

Контролери отримують дані, за потреби кидають AppError, та повертають одну структуру успіху.

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
};

Підключення в app.js

Обробник розміщується останнім.

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;

Що дає вам цей шаблон

Менше повторюваного коду

Контролери більше не повторюються:

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

Один формат помилок

Клієнти можуть покладатися на одну структуру:

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

Фокусовані контролери

Завдання контролера стає простим та зрозумілим:

Get data
↓
Validate
↓
Process
↓
Return result

Єдине місце для контролю стану

Логування, ідентифікатори запитів та відстеження помилок знаходяться в одному мідлвейрі.

Порядок має значення: реєструйте обробник останнім

Він має йти після кожного роутера, який він обробляє:

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

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

Якщо його реєструвати раніше, він ніколи не побачить помилок з пізніших маршрутів.

Підсумок

Замість того, щоб:

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

стає:

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

Справжня мета — розділити бізнес-логіку від відображення помилок:

  • Мідлвейр для обробки помилок потребує чотирьох параметрів та має бути реєстрований останнім.
  • Використовуйте asyncHandler у Express 4; Express 5 самостійно обробляє асинхронні відмови.
  • Викидайте AppError із кодом статусу для очікуваних помилок та приховуйте повідомлення про неочікувані.