← JavaScript/Пользовательские ошибки#83 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

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

Какую проблему решают кастомные ошибки

Ты пишешь REST API для интернет-магазина. Когда что-то идёт не так, нужно не просто бросить ошибку, а сообщить конкретно что произошло: товар не найден? Нет прав доступа? Неверные данные в форме?

Встроенные TypeError и RangeError слишком общие. В реальных проектах создают иерархию собственных классов ошибок — это делает код читаемым, а обработку ошибок — точной.

На основе предыдущих уроков

  • «Наследование» — extends и super() — именно это используется для расширения Error
  • «try/catch» — instanceof в catch для различения типов ошибок
  • «Классы» — конструктор и поля класса
  • Расширяем класс Error

    class ValidationError extends Error {
      constructor(message, field) {
        super(message)                      // передаём message в Error
        this.name = 'ValidationError'       // читаемое имя для логов
        this.field = field                  // дополнительный контекст
      }
    }
    
    const err = new ValidationError('Email некорректен', 'email')
    console.log(err.message)  // 'Email некорректен'
    console.log(err.name)     // 'ValidationError'
    console.log(err.field)    // 'email'
    console.log(err instanceof ValidationError)  // true
    console.log(err instanceof Error)            // true — сохраняется цепочка

    Иерархия ошибок приложения

    В реальных проектах строят дерево: базовый класс для всего приложения, от него — конкретные типы:

    // Базовый класс — все ошибки приложения
    class AppError extends Error {
      constructor(message, statusCode = 500) {
        super(message)
        this.name = 'AppError'
        this.statusCode = statusCode
      }
    }
    
    // 404 — ресурс не найден
    class NotFoundError extends AppError {
      constructor(resource, id) {
        super(`${resource} с id=${id} не найден`, 404)
        this.name = 'NotFoundError'
      }
    }
    
    // 400 — неверные данные от клиента
    class ValidationError extends AppError {
      constructor(message, field) {
        super(message, 400)
        this.name = 'ValidationError'
        this.field = field
      }
    }
    
    // 403 — нет доступа
    class ForbiddenError extends AppError {
      constructor(action) {
        super(`Действие "${action}" запрещено`, 403)
        this.name = 'ForbiddenError'
      }
    }

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

    function getProductById(id, products) {
      if (!Number.isInteger(id) || id <= 0) {
        throw new ValidationError('id должен быть положительным целым числом', 'id')
      }
      const product = products.find(p => p.id === id)
      if (!product) {
        throw new NotFoundError('Product', id)
      }
      return product
    }
    
    // В обработчике запроса:
    try {
      const product = getProductById(req.params.id, db.products)
      res.json(product)
    } catch (e) {
      if (e instanceof NotFoundError) {
        res.status(404).json({ error: e.message })
      } else if (e instanceof ValidationError) {
        res.status(400).json({ error: e.message, field: e.field })
      } else if (e instanceof AppError) {
        res.status(e.statusCode).json({ error: e.message })
      } else {
        throw e  // Неожиданная ошибка — пробрасываем дальше
      }
    }

    Преимущества кастомных ошибок

    1. Читаемость — NotFoundError говорит сама за себя, TypeError — нет

    2. Структурированный контекст — поля field, statusCode несут дополнительные данные

    3. Точная обработка — instanceof NotFoundError vs instanceof AppError

    4. Документация через код — список классов ошибок = список возможных сбоев API

    5. Stack trace — сохраняется как у обычных Error, отладка не страдает

    Типичные ошибки

    1. Забыли this.name — ошибка показывается как "Error":

    // Сломано — имя будет 'Error', а не 'ValidationError':
    class ValidationError extends Error {
      constructor(message, field) {
        super(message)
        // забыли this.name = 'ValidationError'
        this.field = field
      }
    }
    const e = new ValidationError('Ошибка', 'email')
    console.log(e.name)  // 'Error' — неожиданно!
    
    // Исправлено:
    class ValidationError extends Error {
      constructor(message, field) {
        super(message)
        this.name = 'ValidationError'  // обязательно!
        this.field = field
      }
    }

    2. Ловят AppError вместо конкретного подкласса:

    // Сломано — все ошибки обрабатываются одинаково:
    catch (e) {
      if (e instanceof AppError) {
        res.status(500).json({ error: e.message })  // 404 превращается в 500!
      }
    }
    
    // Исправлено — сначала конкретные, потом общий:
    catch (e) {
      if (e instanceof NotFoundError) {
        res.status(404).json({ error: e.message })
      } else if (e instanceof AppError) {
        res.status(e.statusCode).json({ error: e.message })
      }
    }

    3. Бросают строки вместо Error-объектов:

    // Сломано — нет stack trace, нет instanceof:
    throw 'Пользователь не найден'
    
    // Исправлено:
    throw new NotFoundError('User', userId)

    В реальных проектах

  • Express.js: централизованный error middleware различает типы через instanceof
  • NestJS: HttpException и его наследники (NotFoundException, BadRequestException)
  • Apollo GraphQL: ApolloError, AuthenticationError, ForbiddenError
  • Любой сервисный слой: UserNotFoundError, PaymentFailedError, DuplicateEmailError
  • Примеры

    Иерархия ошибок API интернет-магазина с обработкой по типам

    // Иерархия ошибок
    class AppError extends Error {
      constructor(message, statusCode = 500) {
        super(message)
        this.name = 'AppError'
        this.statusCode = statusCode
      }
    }
    
    class NotFoundError extends AppError {
      constructor(resource, id) {
        super(`${resource} с id=${id} не найден`, 404)
        this.name = 'NotFoundError'
        this.resource = resource
      }
    }
    
    class ValidationError extends AppError {
      constructor(message, field) {
        super(message, 400)
        this.name = 'ValidationError'
        this.field = field
      }
    }
    
    class ForbiddenError extends AppError {
      constructor(action) {
        super(`Действие "${action}" запрещено`, 403)
        this.name = 'ForbiddenError'
      }
    }
    
    // Данные "базы данных"
    const products = [
      { id: 1, name: 'MacBook Pro', price: 180000, adminOnly: false },
      { id: 2, name: 'Серверный GPU', price: 900000, adminOnly: true },
    ]
    
    // Сервисный слой
    function getProduct(id, userRole = 'user') {
      if (!id || typeof id !== 'number') {
        throw new ValidationError('id должен быть числом', 'id')
      }
    
      const product = products.find(p => p.id === id)
      if (!product) {
        throw new NotFoundError('Product', id)
      }
    
      if (product.adminOnly && userRole !== 'admin') {
        throw new ForbiddenError('просмотр этого товара')
      }
    
      return product
    }
    
    // Универсальный обработчик ошибок
    function handleRequest(fn) {
      try {
        const result = fn()
        console.log('Успех:', JSON.stringify(result))
      } catch (e) {
        if (e instanceof ValidationError) {
          console.log(`[${e.statusCode}] Валидация (поле "${e.field}"): ${e.message}`)
        } else if (e instanceof NotFoundError) {
          console.log(`[${e.statusCode}] Не найден: ${e.message}`)
        } else if (e instanceof ForbiddenError) {
          console.log(`[${e.statusCode}] Доступ запрещён: ${e.message}`)
        } else if (e instanceof AppError) {
          console.log(`[${e.statusCode}] Ошибка приложения: ${e.message}`)
        } else {
          throw e
        }
      }
    }
    
    handleRequest(() => getProduct(1, 'user'))          // Успех: {...}
    handleRequest(() => getProduct(99, 'user'))          // [404] Не найден: ...
    handleRequest(() => getProduct(2, 'user'))           // [403] Доступ запрещён: ...
    handleRequest(() => getProduct('abc', 'admin'))      // [400] Валидация (поле "id"): ...
    
    // instanceof работает по всей иерархии
    const err = new NotFoundError('Order', 42)
    console.log(err instanceof NotFoundError)  // true
    console.log(err instanceof AppError)       // true
    console.log(err instanceof Error)          // true

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

    Какую проблему решают кастомные ошибки

    Ты пишешь REST API для интернет-магазина. Когда что-то идёт не так, нужно не просто бросить ошибку, а сообщить конкретно что произошло: товар не найден? Нет прав доступа? Неверные данные в форме?

    Встроенные TypeError и RangeError слишком общие. В реальных проектах создают иерархию собственных классов ошибок — это делает код читаемым, а обработку ошибок — точной.

    На основе предыдущих уроков

  • «Наследование» — extends и super() — именно это используется для расширения Error
  • «try/catch» — instanceof в catch для различения типов ошибок
  • «Классы» — конструктор и поля класса
  • Расширяем класс Error

    class ValidationError extends Error {
      constructor(message, field) {
        super(message)                      // передаём message в Error
        this.name = 'ValidationError'       // читаемое имя для логов
        this.field = field                  // дополнительный контекст
      }
    }
    
    const err = new ValidationError('Email некорректен', 'email')
    console.log(err.message)  // 'Email некорректен'
    console.log(err.name)     // 'ValidationError'
    console.log(err.field)    // 'email'
    console.log(err instanceof ValidationError)  // true
    console.log(err instanceof Error)            // true — сохраняется цепочка

    Иерархия ошибок приложения

    В реальных проектах строят дерево: базовый класс для всего приложения, от него — конкретные типы:

    // Базовый класс — все ошибки приложения
    class AppError extends Error {
      constructor(message, statusCode = 500) {
        super(message)
        this.name = 'AppError'
        this.statusCode = statusCode
      }
    }
    
    // 404 — ресурс не найден
    class NotFoundError extends AppError {
      constructor(resource, id) {
        super(`${resource} с id=${id} не найден`, 404)
        this.name = 'NotFoundError'
      }
    }
    
    // 400 — неверные данные от клиента
    class ValidationError extends AppError {
      constructor(message, field) {
        super(message, 400)
        this.name = 'ValidationError'
        this.field = field
      }
    }
    
    // 403 — нет доступа
    class ForbiddenError extends AppError {
      constructor(action) {
        super(`Действие "${action}" запрещено`, 403)
        this.name = 'ForbiddenError'
      }
    }

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

    function getProductById(id, products) {
      if (!Number.isInteger(id) || id <= 0) {
        throw new ValidationError('id должен быть положительным целым числом', 'id')
      }
      const product = products.find(p => p.id === id)
      if (!product) {
        throw new NotFoundError('Product', id)
      }
      return product
    }
    
    // В обработчике запроса:
    try {
      const product = getProductById(req.params.id, db.products)
      res.json(product)
    } catch (e) {
      if (e instanceof NotFoundError) {
        res.status(404).json({ error: e.message })
      } else if (e instanceof ValidationError) {
        res.status(400).json({ error: e.message, field: e.field })
      } else if (e instanceof AppError) {
        res.status(e.statusCode).json({ error: e.message })
      } else {
        throw e  // Неожиданная ошибка — пробрасываем дальше
      }
    }

    Преимущества кастомных ошибок

    1. Читаемость — NotFoundError говорит сама за себя, TypeError — нет

    2. Структурированный контекст — поля field, statusCode несут дополнительные данные

    3. Точная обработка — instanceof NotFoundError vs instanceof AppError

    4. Документация через код — список классов ошибок = список возможных сбоев API

    5. Stack trace — сохраняется как у обычных Error, отладка не страдает

    Типичные ошибки

    1. Забыли this.name — ошибка показывается как "Error":

    // Сломано — имя будет 'Error', а не 'ValidationError':
    class ValidationError extends Error {
      constructor(message, field) {
        super(message)
        // забыли this.name = 'ValidationError'
        this.field = field
      }
    }
    const e = new ValidationError('Ошибка', 'email')
    console.log(e.name)  // 'Error' — неожиданно!
    
    // Исправлено:
    class ValidationError extends Error {
      constructor(message, field) {
        super(message)
        this.name = 'ValidationError'  // обязательно!
        this.field = field
      }
    }

    2. Ловят AppError вместо конкретного подкласса:

    // Сломано — все ошибки обрабатываются одинаково:
    catch (e) {
      if (e instanceof AppError) {
        res.status(500).json({ error: e.message })  // 404 превращается в 500!
      }
    }
    
    // Исправлено — сначала конкретные, потом общий:
    catch (e) {
      if (e instanceof NotFoundError) {
        res.status(404).json({ error: e.message })
      } else if (e instanceof AppError) {
        res.status(e.statusCode).json({ error: e.message })
      }
    }

    3. Бросают строки вместо Error-объектов:

    // Сломано — нет stack trace, нет instanceof:
    throw 'Пользователь не найден'
    
    // Исправлено:
    throw new NotFoundError('User', userId)

    В реальных проектах

  • Express.js: централизованный error middleware различает типы через instanceof
  • NestJS: HttpException и его наследники (NotFoundException, BadRequestException)
  • Apollo GraphQL: ApolloError, AuthenticationError, ForbiddenError
  • Любой сервисный слой: UserNotFoundError, PaymentFailedError, DuplicateEmailError
  • Примеры

    Иерархия ошибок API интернет-магазина с обработкой по типам

    // Иерархия ошибок
    class AppError extends Error {
      constructor(message, statusCode = 500) {
        super(message)
        this.name = 'AppError'
        this.statusCode = statusCode
      }
    }
    
    class NotFoundError extends AppError {
      constructor(resource, id) {
        super(`${resource} с id=${id} не найден`, 404)
        this.name = 'NotFoundError'
        this.resource = resource
      }
    }
    
    class ValidationError extends AppError {
      constructor(message, field) {
        super(message, 400)
        this.name = 'ValidationError'
        this.field = field
      }
    }
    
    class ForbiddenError extends AppError {
      constructor(action) {
        super(`Действие "${action}" запрещено`, 403)
        this.name = 'ForbiddenError'
      }
    }
    
    // Данные "базы данных"
    const products = [
      { id: 1, name: 'MacBook Pro', price: 180000, adminOnly: false },
      { id: 2, name: 'Серверный GPU', price: 900000, adminOnly: true },
    ]
    
    // Сервисный слой
    function getProduct(id, userRole = 'user') {
      if (!id || typeof id !== 'number') {
        throw new ValidationError('id должен быть числом', 'id')
      }
    
      const product = products.find(p => p.id === id)
      if (!product) {
        throw new NotFoundError('Product', id)
      }
    
      if (product.adminOnly && userRole !== 'admin') {
        throw new ForbiddenError('просмотр этого товара')
      }
    
      return product
    }
    
    // Универсальный обработчик ошибок
    function handleRequest(fn) {
      try {
        const result = fn()
        console.log('Успех:', JSON.stringify(result))
      } catch (e) {
        if (e instanceof ValidationError) {
          console.log(`[${e.statusCode}] Валидация (поле "${e.field}"): ${e.message}`)
        } else if (e instanceof NotFoundError) {
          console.log(`[${e.statusCode}] Не найден: ${e.message}`)
        } else if (e instanceof ForbiddenError) {
          console.log(`[${e.statusCode}] Доступ запрещён: ${e.message}`)
        } else if (e instanceof AppError) {
          console.log(`[${e.statusCode}] Ошибка приложения: ${e.message}`)
        } else {
          throw e
        }
      }
    }
    
    handleRequest(() => getProduct(1, 'user'))          // Успех: {...}
    handleRequest(() => getProduct(99, 'user'))          // [404] Не найден: ...
    handleRequest(() => getProduct(2, 'user'))           // [403] Доступ запрещён: ...
    handleRequest(() => getProduct('abc', 'admin'))      // [400] Валидация (поле "id"): ...
    
    // instanceof работает по всей иерархии
    const err = new NotFoundError('Order', 42)
    console.log(err instanceof NotFoundError)  // true
    console.log(err instanceof AppError)       // true
    console.log(err instanceof Error)          // true

    Задание

    Ты разрабатываешь API для системы аутентификации. Создай класс `HttpError extends Error` с полями `statusCode` и стандартным `message`. Создай функцию `authenticate(token, users)`: - Если `token` не строка — `HttpError(400, 'Invalid token format')` - Если token не найден в users — `HttpError(401, 'Unauthorized')` - Если пользователь заблокирован (`blocked: true`) — `HttpError(403, 'Account blocked')` - Иначе возвращает найденного пользователя Обработай все случаи с выводом `statusCode` и `message`.

    Подсказка

    В конструкторе: super(message), затем this.statusCode = statusCode. Для проверки типа: typeof token !== "string". Для поиска: users.find(u => u.token === token). Если !user — 401, если user.blocked — 403.

    Загружаем среду выполнения...
    Загружаем AI-помощника...