JS Tower
Node.jsМодуль 4: HTTP и Express

Обработка ошибок

Централизованная обработка ошибок в Express

Цель урока

В этом уроке ты научишься:

  • Обрабатывать ошибки централизованно
  • Создавать кастомные классы ошибок
  • Логировать ошибки

Error Middleware

Middleware с 4 параметрами обрабатывает ошибки:

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong' });
});

Важно

Error middleware должен быть объявлен последним, после всех маршрутов.


Передача ошибок

Синхронный код

app.get('/user/:id', (req, res, next) => {
  const id = parseInt(req.params.id);
  
  if (isNaN(id)) {
    // Передаём ошибку в error middleware
    return next(new Error('Invalid ID'));
  }
  
  res.json({ id });
});

Асинхронный код

app.get('/users', async (req, res, next) => {
  try {
    const users = await User.find();
    res.json(users);
  } catch (error) {
    next(error);
  }
});

// Или с обёрткой
const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

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

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

Базовый класс

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
    this.isOperational = true;
    
    Error.captureStackTrace(this, this.constructor);
  }
}

module.exports = AppError;

Специфичные ошибки

class NotFoundError extends AppError {
  constructor(message = 'Resource not found') {
    super(message, 404);
  }
}

class ValidationError extends AppError {
  constructor(message = 'Validation failed') {
    super(message, 400);
  }
}

class UnauthorizedError extends AppError {
  constructor(message = 'Unauthorized') {
    super(message, 401);
  }
}

class ForbiddenError extends AppError {
  constructor(message = 'Forbidden') {
    super(message, 403);
  }
}

Использование

const { NotFoundError, ValidationError } = require('./errors');

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

app.post('/users', asyncHandler(async (req, res) => {
  const { email } = req.body;
  
  if (!email) {
    throw new ValidationError('Email is required');
  }
  
  const user = await User.create(req.body);
  res.status(201).json(user);
}));

Централизованный обработчик

const errorHandler = (err, req, res, next) => {
  // Логирование
  console.error('Error:', {
    message: err.message,
    stack: err.stack,
    path: req.path,
    method: req.method
  });
  
  // Операционные ошибки (ожидаемые)
  if (err.isOperational) {
    return res.status(err.statusCode).json({
      status: err.status,
      message: err.message
    });
  }
  
  // Ошибки валидации Mongoose
  if (err.name === 'ValidationError') {
    const messages = Object.values(err.errors).map(e => e.message);
    return res.status(400).json({
      status: 'fail',
      message: messages.join(', ')
    });
  }
  
  // Ошибка дублирования (MongoDB)
  if (err.code === 11000) {
    return res.status(400).json({
      status: 'fail',
      message: 'Duplicate field value'
    });
  }
  
  // JWT ошибки
  if (err.name === 'JsonWebTokenError') {
    return res.status(401).json({
      status: 'fail',
      message: 'Invalid token'
    });
  }
  
  // Неожиданные ошибки
  res.status(500).json({
    status: 'error',
    message: process.env.NODE_ENV === 'production' 
      ? 'Something went wrong' 
      : err.message
  });
};

// Подключение
app.use(errorHandler);

404 Handler

// После всех маршрутов, перед error handler
app.use((req, res, next) => {
  next(new NotFoundError(`Route ${req.originalUrl} not found`));
});

app.use(errorHandler);

Необработанные ошибки

// Необработанные промисы
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
  // Graceful shutdown
  server.close(() => {
    process.exit(1);
  });
});

// Необработанные исключения
process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  process.exit(1);
});

Полная структура

// app.js
const express = require('express');
const { NotFoundError } = require('./errors');
const errorHandler = require('./middleware/errorHandler');

const app = express();

// Middleware
app.use(express.json());

// Routes
app.use('/api/users', require('./routes/users'));
app.use('/api/posts', require('./routes/posts'));

// 404 handler
app.use((req, res, next) => {
  next(new NotFoundError());
});

// Error handler
app.use(errorHandler);

module.exports = app;

Практика

Задание 1: Кастомная ошибка

Задача: Создай ошибку ConflictError для дублирования данных.

Решение:

class ConflictError extends AppError {
  constructor(message = 'Resource already exists') {
    super(message, 409);
  }
}

// Использование
app.post('/users', asyncHandler(async (req, res) => {
  const existing = await User.findOne({ email: req.body.email });
  
  if (existing) {
    throw new ConflictError('Email already registered');
  }
  
  const user = await User.create(req.body);
  res.status(201).json(user);
}));

Проверь себя

  1. Сколько параметров у error middleware?
  2. Как передать ошибку из async функции?
  3. Что такое операционная ошибка?