Express.jsMiddleCoding

Как создать кастомный middleware для обработки ошибок в Express?

Error middleware в Express определяется строго по четырём параметрам (err, req, res, next), регистрируется последним через app.use() и должен вызывать res.json() без повторного вызова next.

Кастомный error middleware в Express

Error middleware в Express отличается от обычного тем, что принимает четыре аргумента: (err, req, res, next). Express определяет его именно по сигнатуре — если убрать хотя бы один параметр, функция перестаёт работать как error handler.

Базовая структура

import { Request, Response, NextFunction } from 'express';

export function errorMiddleware(
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction   // обязателен, даже если не используется
): void {
  console.error(`[${new Date().toISOString()}] ${req.method} ${req.path}`, err);

  const status = (err as any).status ?? (err as any).statusCode ?? 500;
  const message =
    process.env.NODE_ENV === 'production' && status === 500
      ? 'Internal server error'
      : err.message;

  res.status(status).json({ error: message });
}

Кастомные классы ошибок

// errors/HttpError.ts
export class HttpError extends Error {
  constructor(
    public readonly status: number,
    message: string,
    public readonly code?: string
  ) {
    super(message);
    this.name = 'HttpError';
  }
}

export class NotFoundError extends HttpError {
  constructor(resource = 'Resource') {
    super(404, `${resource} not found`, 'NOT_FOUND');
  }
}

export class ValidationError extends HttpError {
  constructor(
    message: string,
    public readonly fields?: Record<string, string>
  ) {
    super(422, message, 'VALIDATION_ERROR');
  }
}
// routes/users.ts
import { NotFoundError, ValidationError } from '../errors/HttpError';

router.get('/users/:id', asyncHandler(async (req, res) => {
  const { id } = req.params;
  if (!/^\d+$/.test(id)) throw new ValidationError('id must be a number');

  const user = await userService.findById(Number(id));
  if (!user) throw new NotFoundError('User');

  res.json(user);
}));

Полный error middleware с разными типами ошибок

// middleware/errorMiddleware.ts
import { Request, Response, NextFunction } from 'express';
import { HttpError } from '../errors/HttpError';
import { ZodError } from 'zod';

export function errorMiddleware(
  err: unknown,
  req: Request,
  res: Response,
  next: NextFunction
): void {
  // Zod validation error
  if (err instanceof ZodError) {
    res.status(422).json({
      error: 'Validation failed',
      fields: err.errors.map(e => ({ path: e.path.join('.'), message: e.message })),
    });
    return;
  }

  // Кастомные HTTP-ошибки
  if (err instanceof HttpError) {
    res.status(err.status).json({
      error: err.message,
      code: err.code,
    });
    return;
  }

  // pg unique violation
  if ((err as any).code === '23505') {
    res.status(409).json({ error: 'Resource already exists' });
    return;
  }

  // Неизвестная ошибка
  console.error('Unhandled error:', err);
  res.status(500).json({
    error: process.env.NODE_ENV === 'production'
      ? 'Internal server error'
      : (err instanceof Error ? err.message : String(err)),
  });
}

Регистрация в app.ts

import express from 'express';
import { userRouter } from './routes/users';
import { errorMiddleware } from './middleware/errorMiddleware';

const app = express();
app.use(express.json());
app.use('/api/users', userRouter);

// 404 handler — ПЕРЕД error middleware, ПОСЛЕ маршрутов
app.use((req, res) => {
  res.status(404).json({ error: `Route ${req.method} ${req.path} not found` });
});

// Error middleware — ПОСЛЕДНИМ
app.use(errorMiddleware);

export default app;

Подводные камни

  • Сигнатура без 4 параметров: если написать (err, req, res), Express не распознает это как error middleware и ошибки не будут обрабатываться.
  • Регистрация не последней: error middleware должен быть после всех маршрутов и обычных middleware, иначе до него просто не дойдёт выполнение.
  • Не вызывать next() после res.json(): если уже отправили ответ и потом вызовете next(err) — получите «Cannot set headers after they are sent»; проверяйте res.headersSent.
  • Утечка стека ошибок в production: err.stack содержит пути файловой системы и версии зависимостей — никогда не отправляйте его клиенту в боевом окружении.
  • Тип err = any/unknown: в TypeScript err может быть чем угодно (не только Error), поэтому проверяйте через instanceof перед доступом к свойствам.
  • Нет обработки ошибок парсинга JSON: если express.json() получит невалидный JSON, он вызовет next с объектом ошибки типа SyntaxError со статусом 400 — обработайте его явно в error middleware.
  • Отсутствие логирования: молчаливое поглощение ошибок без console.error или внешнего логгера (Pino, Winston) делает отладку production-проблем почти невозможной.

Common mistakes

  • Дает общий ответ про Node.js и не называет конкретные API Express.js.
  • Не объясняет, где в lifecycle находится кастомный error-handling middleware.
  • Не разделяет validation, authorization, business logic и persistence.
  • Игнорирует ошибки, лимиты входных данных, observability и тестирование.

What the interviewer is testing

  • Может объяснить кастомный error-handling middleware на примере кода.
  • Называет ключевые API: app.use(errorHandler).
  • Использует точные API Express.js, а не вымышленные hooks/decorators/methods.
  • Видит production-риски: безопасность, отказоустойчивость, логирование и тесты.

Sources

Related topics

Как создать кастомный middleware для обработки ошибок в Express? | Talanto