Пользовательские ошибки

Пользовательские ошибки — классы, расширяющие встроенный Error, которые позволяют создавать семантически значимые типы ошибок для конкретной предметной области приложения.

Зачем нужно

Стандартный Error с произвольным message неудобен для программной обработки: приходится парсить строку, чтобы понять причину сбоя. Пользовательские классы ошибок позволяют точно проверить тип (instanceof ValidationError), добавить структурированные данные и выстроить иерархию ошибок.

Где используется

  • Валидация данных форм и API
  • Бизнес-логика (недостаточно прав, лимит превышен)
  • HTTP-ошибки в серверных приложениях (Node.js, Express)
  • Слой доступа к данным (NotFoundError, DatabaseError)

Базовый пользовательский класс ошибки

class AppError extends Error {
  constructor(message, code) {
    super(message);           // устанавливает this.message
    this.name = 'AppError';   // имя типа ошибки
    this.code = code;

    // Восстанавливаем стек вызовов (важно в V8)
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }
  }
}

const err = new AppError('Что-то пошло не так', 'INTERNAL');
console.log(err.name);    // AppError
console.log(err.message); // Что-то пошло не так
console.log(err.code);    // INTERNAL
console.log(err instanceof AppError); // true
console.log(err instanceof Error);    // true

Иерархия ошибок

// Базовый класс домена
class DomainError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name; // автоматически из имени класса
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }
  }
}

// Конкретные типы
class ValidationError extends DomainError {
  constructor(field, message) {
    super(message);
    this.field = field;
  }
}

class NotFoundError extends DomainError {
  constructor(resource, id) {
    super(`${resource} с id=${id} не найден`);
    this.resource = resource;
    this.id = id;
  }
}

class AuthError extends DomainError {
  constructor(message = 'Нет доступа') {
    super(message);
    this.statusCode = 403;
  }
}

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

function validateAge(age) {
  if (typeof age !== 'number') {
    throw new ValidationError('age', 'Возраст должен быть числом');
  }
  if (age < 0 || age > 150) {
    throw new ValidationError('age', 'Возраст вне допустимого диапазона');
  }
}

async function getUser(id) {
  const user = await db.findById(id);
  if (!user) throw new NotFoundError('User', id);
  return user;
}

// Обработка с instanceof
try {
  validateAge('не число');
} catch (err) {
  if (err instanceof ValidationError) {
    console.error(`Поле '${err.field}': ${err.message}`);
    // Показать пользователю
  } else if (err instanceof NotFoundError) {
    console.error(`Ресурс '${err.resource}' не найден`);
  } else {
    throw err; // непредвиденная ошибка — пробрасываем дальше
  }
}

HTTP-ошибки (Node.js / Express)

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

class BadRequestError extends HttpError {
  constructor(msg = 'Bad Request') { super(400, msg); }
}

class UnauthorizedError extends HttpError {
  constructor(msg = 'Unauthorized') { super(401, msg); }
}

class ForbiddenError extends HttpError {
  constructor(msg = 'Forbidden') { super(403, msg); }
}

// Middleware Express
app.use((err, req, res, next) => {
  if (err instanceof HttpError) {
    return res.status(err.statusCode).json({ error: err.message });
  }
  res.status(500).json({ error: 'Internal Server Error' });
});

Частые ошибки

1. Не установлено this.name

class MyError extends Error {
  constructor(msg) {
    super(msg);
    // Без this.name = 'MyError'
    // err.name будет 'Error', не 'MyError'
  }
}

2. Потеря стека при пробрасывании

// Плохо: исходный стек теряется
catch (err) { throw new Error(err.message); }

// Хорошо: сохраняем причину
catch (err) { throw new AppError('Ошибка', 'CODE', { cause: err }); }

Связанные темы

Ресурсы