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

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

Функции промежуточного обработчика для обработки ошибок определяются так же, как и другие функции промежуточной обработки, но с указанием для функции обработки ошибок не трех, а четырех аргументов: (err, req, res, next). Например:


app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

Промежуточный обработчик для обработки ошибок должен быть определен последним, после указания всех app.use() и вызовов маршрутов; например:


var bodyParser = require('body-parser');
var methodOverride = require('method-override');

app.use(bodyParser());
app.use(methodOverride());
app.use(function(err, req, res, next) {
  // logic
});

Ответы, поступающие из функции промежуточной обработки, могут иметь любой формат, в зависимости от ваших предпочтений. Например, это может быть страница сообщения об ошибке HTML, простое сообщение или строка JSON.

В целях упорядочения (и для фреймворков более высокого уровня) можно определить несколько функций промежуточной обработки ошибок, точно так же, как это допускается для обычных функций промежуточной обработки. Например, для того чтобы определить обработчик ошибок для запросов, совершаемых с помощью XHR, и для остальных запросов, можно воспользоваться следующими командами:


var bodyParser = require('body-parser');
var methodOverride = require('method-override');

app.use(bodyParser());
app.use(methodOverride());
app.use(logErrors);
app.use(clientErrorHandler);
app.use(errorHandler);

В данном примере базовый код logErrors может записывать информацию о запросах и ошибках в stderr, например:


function logErrors(err, req, res, next) {
  console.error(err.stack);
  next(err);
}

Кроме того, в данном примере clientErrorHandler определен, как указано ниже; в таком случае ошибка явным образом передается далее следующему обработчику:


function clientErrorHandler(err, req, res, next) {
  if (req.xhr) {
    res.status(500).send({ error: 'Something failed!' });
  } else {
    next(err);
  }
}

“Обобщающая” функция errorHandler может быть реализована так:


function errorHandler(err, req, res, next) {
  res.status(500);
  res.render('error', { error: err });
}

При передаче какого-либо объекта в функцию next() (кроме строки 'route'), Express интерпретирует текущий запрос как ошибку и пропустит все остальные функции маршрутизации и промежуточной обработки, не являющиеся функциями обработки ошибок. Для того чтобы обработать данную ошибку определенным образом, необходимо создать маршрут обработки ошибок, как описано в следующем разделе.

Если задан обработчик ошибок с несколькими функциями обратного вызова, можно воспользоваться параметром route, чтобы перейти к следующему обработчику маршрута. Например:


app.get('/a_route_behind_paywall',
  function checkIfPaidSubscriber(req, res, next) {
    if(!req.user.hasPaid) {

      // continue handling this request
      next('route');
    }
  }, function getPaidContent(req, res, next) {
    PaidContent.find(function(err, doc) {
      if(err) return next(err);
      res.json(doc);
    });
  });

В данном примере обработчик getPaidContent будет пропущен, но выполнение всех остальных обработчиков в app для /a_route_behind_paywall будет продолжено.

Вызовы next() и next(err) указывают на завершение выполнения текущего обработчика и на его состояние. next(err) пропускает все остальные обработчики в цепочке, кроме заданных для обработки ошибок, как описано выше.

Стандартный обработчик ошибок

В Express предусмотрен встроенный обработчик ошибок, который обрабатывает любые возможные ошибки, встречающиеся в приложении. Этот стандартный обработчик ошибок добавляется в конец стека функций промежуточной обработки.

В случае передачи ошибки в next() без обработки с помощью обработчика ошибок, такая ошибка будет обработана встроенным обработчиком ошибок. Ошибка будет записана на клиенте с помощью трассировки стека. Трассировка стека не включена в рабочую среду.

Для запуска приложения в рабочем режиме необходимо задать для переменной среды NODE_ENV значение production.

При вызове next() с ошибкой после начала записи ответа
(например, если ошибка обнаружена во время включения ответа в поток, направляемый клиенту), стандартный обработчик ошибок Express закрывает соединение и отклоняет запрос.

Поэтому при добавлении нестандартного обработчика ошибок вам потребуется делегирование в стандартные
механизмы обработки ошибок в Express в случае, если заголовки уже были отправлены клиенту:


function errorHandler(err, req, res, next) {
  if (res.headersSent) {
    return next(err);
  }
  res.status(500);
  res.render('error', { error: err });
}

Обработка ошибок — это боль. Вы можете довольно далеко продвинуться без правильной обработки ошибок, но чем больше приложение, тем с большими проблемами вы столкнетесь. Чтобы действительно вывести вашу разработку API на новый уровень, вам следует взяться за решение этой задачи. Обработка ошибок — это обширная тема, и ее можно выполнять разными способами, в зависимости от приложения, технологий и многого другого. Это одна из тех вещей, которые легко понять, но трудно понять полностью.

Что мы будем делать

В этой статье мы собираемся объяснить удобный для новичков способ обработки ошибок в API Node.js + Express.js с помощью TypeScript. Мы собираемся объяснить, что такое ошибка, различные типы ошибок, которые могут возникнуть, и как их обрабатывать в нашем приложении. Вот некоторые вещи, которыми мы займемся в следующих главах:

  1. узнаем, что на самом деле такое «обработка ошибок» и с какими типами ошибок вы столкнетесь
  2. узнаем об объекте Node.js Error и о том, как его использовать
  3. узнаем, как создавать собственные классы ошибок и как они могут помочь нам в разработке лучших API-интерфейсов и Node-приложений
  4. узнаем о промежуточном программном обеспечении Express и о том, как его использовать для обработки наших ошибок
  5. научимся структурировать информацию об ошибках и представлять ее потребителю и разработчику

Предусловие

Предупреждение! В этой статье предполагается, что вы уже кое-что знаете. Несмотря на то, что это удобно для новичков, вот что вам следует знать, чтобы извлечь из этой статьи максимальную пользу:

  1. рабочее знание Node.js
  2. рабочее знание Express.js (маршруты, промежуточное ПО и т. д.)
  3. основы TypeScript (и классы!)
  4. основы работы API и его написания с использованием Express.js

Хорошо. Мы можем начинать.

Что такое обработка ошибок и зачем она вам нужна?

Так что же такое «обработка ошибок» на самом деле?

Обработка ошибок (или обработка исключений) — это процесс реагирования на возникновение ошибок (аномальное/нежелательное поведение) во время выполнения программы.

Зачем нужна обработка ошибок?

Потому что мы хотим сделать исправление ошибок менее болезненным. Это также помогает нам писать более чистый код, поскольку весь код обработки ошибок централизован, вместо того, чтобы обрабатывать ошибки там, где мы думаем, что они могут возникнуть. В конце концов — код более организован, вы меньше повторяетесь, и это сокращает время разработки и обслуживания.

Типы ошибок

Есть два основных типа ошибок, которые нам необходимо различать и соответственно обрабатывать.

Операционные ошибки

Операционные ошибки представляют собой проблемы во время выполнения. Это не обязательно «ошибки», это внешние обстоятельства, которые могут нарушить ход выполнения программы. Несмотря на то, что они не являются ошибками в вашем коде, эти ситуации могут (и неизбежно будут) возникать, и с ними нужно обращаться. Вот некоторые примеры:

  1. Запрос API не выполняется по какой-либо причине (например, сервер не работает или превышен лимит скорости)
  2. Невозможно установить соединение с базой данных
  3. Пользователь отправляет неверные входные данные
  4. Системе не хватает памяти

Ошибки программиста

Ошибки программиста — это настоящие «ошибки», поэтому они представляют собой проблемы в самом коде. Как ошибки в синтаксисе или логике программы, они могут быть устранены только путем изменения исходного кода. Вот несколько примеров ошибок программиста:

  1. Попытка прочитать свойство объекта, которое не определено
  2. Передача некорректных параметров в функции
  3. не улавливая отвергнутого обещания

Что такое ошибка узла?

В Node.js есть встроенный объект Error который мы будем использовать в качестве основы для выдачи ошибок. При вызове он содержит набор информации, которая сообщает нам, где произошла ошибка, тип ошибки и в чем проблема. В документации Node.js есть более подробное объяснение.

Мы можем создать такую ​​ошибку:

const error = new Error('Error message');

Итак, мы дали ему строковый параметр, который будет сообщением об ошибке. Но что еще есть в этом Error? Поскольку мы используем typescript, мы можем проверить его определение, что приведет нас к interface typescript:

const error = new Error('Error message');

Name и message не требуют пояснений, а stack содержит namemessage и строку, описывающую точку в коде, в которой был создан  Error. Этот стек на самом деле представляет собой серию стековых фреймов (подробнее о нем можно узнать здесь). Каждый фрейм описывает сайт вызова в коде, который приводит к сгенерированной ошибке. Мы можем вызвать стек console.log(),

и посмотрим, что он может нам сказать. Вот пример ошибки, которую мы получаем при передаче строки в качестве аргумента функции JSON.parse() (которая завершится ошибкой, поскольку JSON.parse() принимает только данные JSON в строковом формате):

Как мы видим, это ошибка типа SyntaxError с сообщением « Неожиданный токен A в JSON в позиции 0 ». Внизу мы видим кадры стека. Это ценная информация, которую мы, как разработчик, можем использовать для отладки нашего кода, определения проблемы и ее устранения.

Написание собственных классов ошибок.

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

Как я упоминал ранее, мы можем использовать встроенный объект Error, поскольку он дает нам ценную информацию.

Однако при написании нашего API нам часто требуется предоставить нашим разработчикам и потребителям API немного больше информации, чтобы мы могли облегчить их (и нашу) жизнь.

Для этого мы можем написать класс, который расширит класс Error, добавив в него немного больше данных.

class BaseError extends Error {
  statusCode: number;

  constructor(statusCode: number, message: string) {
    super(message);

    Object.setPrototypeOf(this, new.target.prototype);
    this.name = Error.name;
    this.statusCode = statusCode;
    Error.captureStackTrace(this);
  }
}

Здесь мы создаем класс BaseError, расширяющий этот класс Error. Объект принимает statusCode(код состояния HTTP, который мы вернем пользователю) и message(сообщение об ошибке, как при создании встроенного объекта Node Error).

Теперь мы можем использовать класс Node BaseError вместо класса Error для добавления кода состояния HTTP.

// Import the class
import { BaseError } from '../utils/error';

const extendedError = new BaseError(400, 'message');

Мы будем использовать этот класс BaseError в качестве основы для всех наших пользовательских ошибок.

Теперь мы можем использовать класс BaseError, чтобы расширить его и сформировать все наши собственные ошибки. Это зависит от потребностей нашего приложения. Например, если мы собираемся иметь конечные точки аутентификации в нашем API, мы можем расширить класс BaseError и создать класс AuthenticationError:

class AuthenticationError extends BaseError {}

Он будет использовать тот же конструктор, что и наш BaseError, но как только мы используем его в нашем коде, он значительно упростит чтение и отладку кода.

Теперь, когда мы знаем, как расширить объект Error, мы можем сделать еще один шаг.

Типичная ошибка, которая может нам понадобиться, — это ошибка «не найдено». Допустим, у нас есть конечная точка, где пользователь указывает идентификатор продукта, и мы пытаемся получить его из базы данных. В случае, если мы не получим результатов по этому идентификатору, мы хотим сообщить пользователю, что продукт не был найден.

Поскольку мы, вероятно, собираемся использовать ту же логику не только для продуктов (например, для пользователей, карт, местоположений), давайте сделаем эту ошибку повторно используемой.

Давайте расширим класс BaseError, но теперь давайте сделаем код состояния по умолчанию 404 и поместим аргумент «свойство» в конструктор:

class NotFoundError extends BaseError {
  propertyName: string;

  constructor(propertyName: string) {
    super(404, `Property '${propertyName}' not found.`);

    this.propertyName = propertyName;
  }
}

Теперь при использовании класса NotFoundError мы можем просто дать ему имя свойства, и объект создаст для нас полное сообщение (statusCode по умолчанию будет 404, как вы можете видеть из кода).

// This is how we can use the error
const notFoundError = new NotFoundError('Product');

А вот как это выглядит при сбросе:

Теперь мы можем создавать различные ошибки в соответствии с нашими потребностями. Вот некоторые из наиболее распространенных примеров API:

  1. ValidationError (ошибки, которые можно использовать при обработке входящих пользовательских данных)
  2. DatabaseError (ошибки, которые вы можете использовать, чтобы сообщить пользователю, что существует проблема с взаимодействием с базой данных)
  3. AuthenticationError (ошибка, которую можно использовать, чтобы сообщить пользователю об ошибке аутентификации)

Идем на шаг дальше

Вооружившись этими знаниями, вы сможете сделать еще один шаг вперед. В зависимости от ваших потребностей вы можете добавить errorCode в класс BaseError, а затем использовать его в некоторых из ваших пользовательских классов ошибок, чтобы сделать ошибки более удобочитаемыми для потребителя.

Например, вы можете использовать коды ошибок в AuthenticationError чтобы сообщить потребителю тип ошибки аутентификации. A01 может означать, что пользователь не проверен, а A02 может означать, что срок действия ссылки для сброса пароля истек.

Подумайте о потребностях вашего приложения и постарайтесь сделать его как можно проще.

Создание и обнаружение ошибок в контроллерах

Теперь давайте посмотрим на образец контроллера (функцию маршрута) в Express.js.

const sampleController = (req: Request, res: Response, next: NextFunction) => {

  res.status(200).json({
    response: 'successfull',
    data: {
      answer: 42
    }
  });
};

Попробуем использовать наш собственный класс ошибок NotFoundError. Давайте воспользуемся функцией next (), чтобы передать наш настраиваемый объект ошибки следующей функции промежуточного программного обеспечения, которая перехватит ошибку и позаботится о ней (не беспокойтесь об этом, я объясню, как отлавливать ошибки через минуту).

const sampleController = async (req: Request, res: Response, next: NextFunction) => {

    return next(new NotFoundError('Product'))

  res.status(200).json({
    response: 'successfull',
    data: {
      answer: 42
    }
  });
};

Это успешно остановит выполнение этой функции и передаст ошибку следующей функции промежуточного программного обеспечения. Так вот оно?

Не совсем. Нам все еще нужно обрабатывать ошибки, которые мы не обрабатываем с помощью наших пользовательских ошибок.

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

Например, предположим, что вы пишете фрагмент кода, который проходит все проверки синтаксиса, но выдает ошибку во время выполнения. Эти ошибки могут случиться, и они будут. Как мы с ними справляемся?

Допустим, вы хотите использовать функцию JSON.parse(). Эта функция принимает данные JSON в виде строки, но вы даете ей случайную строку. Если передать этой функции, основанной на обещаниях, строку, она выдаст ошибку! Если не обработать, это вызовет ошибку UnhandledPromiseRejectionWarning.

Что ж, просто оберните свой код в блок try/catch и передайте любые ошибки по строке промежуточного программного обеспечения, используя next()(опять же, я скоро объясню это)!

И это действительно сработает. Это неплохая практика, поскольку все ошибки, возникающие из-за кода, основанного на обещаниях, будут обнаружены внутри блока .catch(). Однако у этого есть обратная сторона, а именно тот факт, что ваши файлы контроллера будут заполнены повторяющимися блоками try/catch, и мы не хотим повторяться. К счастью, у нас есть еще один козырь в рукаве.

Оболочка handleAsync

Поскольку мы не хотим писать наши блоки try/catch в каждом контроллере (функция маршрутизации), мы можем написать функцию промежуточного программного обеспечения, которая выполняет это один раз, а затем применяет ее к каждому контроллеру.

Вот как это выглядит:

const asyncHandler = (fn: any) => (req: Request, res: Response, next: NextFunction) => Promise.resolve(fn(req, res, next)).catch(next);

На первый взгляд это может показаться сложным, но это всего лишь функция промежуточного программного обеспечения, которая действует как блок try /catch с next(err) внутри catch(). Теперь мы можем просто обернуть его вокруг наших контроллеров, и все!

const sampleController = asyncHandler(async (req: Request, res: Response, next: NextFunction) => {
  JSON.parse('A string');

  res.status(200).json({
    response: 'successfull',
    data: {
      something: 2
    }
  });
});

Теперь, если возникнет такая же ошибка, мы не получим UnhandledPromiseRejectionWarning, вместо этого наш код обработки ошибок успешно ответит и зарегистрирует ошибку (разумеется, после того, как мы закончим ее писать. Вот как это будет выглядеть):

Как мне обрабатывать ошибки?

Хорошо, мы научились формировать ошибки. Что теперь?

Теперь нам нужно выяснить, как на самом деле с ними справиться.

Экспресс промежуточное ПО

Экспресс-приложение, по сути, представляет собой серию вызовов функций промежуточного программного обеспечения. Функция промежуточного программного обеспечения имеет доступ к объекту request, объекту response и функции next промежуточного программного обеспечения.

Экспресс с маршрутизацией каждого входящего запроса через эти промежуточные программы, начиная с первого по цепочке, до тех пор, пока ответ не будет отправлен клиенту. Каждая функция промежуточного программного обеспечения может либо передать запрос следующему промежуточному программному обеспечению с помощью функции next(), либо ответить клиенту и разрешить запрос.

Выявление ошибок в Express

В Express есть специальный тип функции промежуточного программного обеспечения, называемый «промежуточное программное обеспечение для обработки ошибок». У этих функций есть дополнительный аргумент err. Каждый раз, когда в функции промежуточного программного обеспечения next() передается ошибка, Express пропускает все функции промежуточного программного обеспечения и сразу переходит к функциям обработки ошибок.

Вот пример того, как его написать:

const errorMiddleware = (error: any, req: Request, res: Response, next: NextFunction) => {
  // Do something with the error
  next(error); // pass it to the next function
};

Что делать с ошибками

Теперь, когда мы знаем, как обнаруживать ошибки, мы должны что-то с ними делать. В API обычно нужно сделать две вещи: ответить клиенту и записать ошибку.

errorReponse промежуточное ПО (ответ клиенту)

Лично при написании API я следую согласованной структуре ответов JSON для успешных и неудачных запросов:

// Success
{
    "response": "successfull",
    "message": "some message if required",
    "data": {}
}

// Failure
{
    "response": "error",
      "error": {
        "type": "type of error",
        "path": "/path/on/which/it/happened",
        "statusCode": 404,
        "message": "Message that describes the situation"
      }
}

А теперь мы собираемся написать промежуточное программное обеспечение, которое обрабатывает часть сбоя.

const errorResponse = (error: any, req: Request, res: Response, next: NextFunction) => {
  const customError: boolean = error.constructor.name === 'NodeError' || error.constructor.name === 'SyntaxError' ? false : true;

  res.status(error.statusCode || 500).json({
    response: 'Error',
    error: {
      type: customError === false ? 'UnhandledError' : error.constructor.name,
      path: req.path,
      statusCode: error.statusCode || 500,
      message: error.message
    }
  });
  next(error);
};

Давайте рассмотрим функцию. Сначала мы создаем логическое значение customError. Мы проверяем свойство error.constructor.name, которое сообщает нам, с какой ошибкой мы имеем дело. Если error.constructor.name это NodeError(или какая-то другая ошибка, которую мы не создавали лично), мы устанавливаем логическое значение false, в противном случае мы устанавливаем true. Таким образом, мы можем по-разному обрабатывать известные и неизвестные ошибки.

Далее мы можем ответить клиенту. Мы используем res.status() для установки кода состояния HTTP и используем функцию res.json() для отправки данных JSON клиенту. При записи данных JSON мы можем использовать логическое значение customError для установки определенных свойств. Например, если логическое значение false customError, мы установим тип ошибки UnhandledError, сообщая пользователю, что мы не ожидали этой ситуации, в противном случае мы устанавливаем его на error.constructor.name.

Поскольку свойство statusCode доступно только в наших настраиваемых объектах ошибок, мы можем просто вернуть 500, если оно недоступно (то есть это необработанная ошибка).

В конце концов, мы используем функцию next() для передачи ошибки следующему промежуточному программному обеспечению.

errorLog промежуточное ПО (регистрация ошибки)

const errorLogging = (error: any, req: Request, res: Response, next: NextFunction) => {
  const customError: boolean = error.constructor.name === 'NodeError' || error.constructor.name === 'SyntaxError' ? false : true;

  console.log('ERROR');
  console.log(`Type: ${error.constructor.name === 'NodeError' ? 'UnhandledError' : error.constructor.name}`);
  console.log('Path: ' + req.path);
  console.log(`Status code: ${error.statusCode || 500}`);
  console.log(error.stack);
};

Эта функция следует той же логике, что и предыдущая, с небольшой разницей. Поскольку это ведение журнала предназначено для разработчиков API, мы также регистрируем стек.

Как вы можете видеть, это будет просто console.log() данные об ошибке в системной консоли. В большинстве производственных API ведение журнала немного более продвинуто, запись в файл или запись в API. Поскольку эта часть построения API очень специфична для конкретного приложения, я не хотел слишком углубляться в нее. Теперь, когда у вас есть данные, выберите, какой подход лучше всего подходит для вашего приложения, и реализуйте свою версию ведения журнала. Если вы развертываетесь в облачной службе развертывания, такой как AWS, вы сможете загружать файлы журналов, просто используя функцию промежуточного программного обеспечения, описанную выше (AWS сохраняет все файлы console.log().

Теперь вы можете обрабатывать ошибки.

Вот так! Этого должно быть достаточно, чтобы вы начали обрабатывать ошибки в рабочем процессе TypeScript + Node.js + Express.js API. Обратите внимание, здесь есть много возможностей для улучшения. Этот подход не является лучшим и не самым быстрым, но он довольно прост и, что наиболее важно, снисходителен, и его быстро можно итерировать и улучшать по мере развития вашего проекта API, требующего большего от ваших навыков. Эти концепции очень важны, и с ними легко начать, и я надеюсь, что вам понравилась моя статья и вы узнали что-то новое.

Когда я только начинал работать с Express и пытался разобраться с тем, как обрабатывать ошибки, мне пришлось нелегко. Возникало такое ощущение, будто никто не писал о том, что мне было нужно. В итоге мне пришлось самому искать ответы на мои вопросы. Сегодня я хочу рассказать всё, что знаю об обработке ошибок в Express-приложениях. Начнём с синхронных ошибок.

Обработка синхронных ошибок

Если вам нужно обработать синхронную ошибку, то вы можете, для начала, с помощью инструкции throw, выдать такую ошибку в обработчике запроса Express. Обратите внимание на то, что обработчики запросов ещё называют «контроллерами», но я предпочитаю использовать термин «обработчик запросов» так как он кажется мне понятнее.

Вот как это выглядит:

app.post('/testing', (req, res) => {
  throw new Error('Something broke! ')
})

Такие ошибки можно перехватить с помощью обработчика ошибок Express. Если вы не написали собственный обработчик ошибок (подробнее об этом мы поговорим ниже), то Express обработает ошибку с помощью обработчика, используемого по умолчанию.

Вот что делает стандартный обработчик ошибок Express:

  1. Устанавливает код состояния HTTP-ответа в значение 500.
  2. Отправляет сущности, выполнившей запрос, текстовый ответ.
  3. Логирует текстовый ответ в консоль.

Сообщение об ошибке, выведенное в консоль

Обработка асинхронных ошибок

Для обработки асинхронных ошибок нужно отправить ошибку обработчику ошибок Express через аргумент next:

app.post('/testing', async (req, res, next) => {
  return next(new Error('Something broke again! '))
})

Вот что попадёт в консоль при логировании этой ошибки.

Сообщение об ошибке, выведенное в консоль

Если вы пользуетесь в Express-приложении конструкцией async/await, то вам понадобится использовать функцию-обёртку, наподобие express-async-handler. Это позволяет писать асинхронный код без блоков try/catch. Подробнее об async/await в Express можно почитать здесь.

const asyncHandler = require('express-async-handler')

app.post('/testing', asyncHandler(async (req, res, next) => {
  // Сделать что-нибудь
}))

После того, как обработчик запроса обёрнут в express-async-handler, то можно, так же, как было описано выше, выбросить ошибку с использованием инструкции throw. Эта ошибка попадёт к обработчику ошибок Express.

app.post('/testing', asyncHandler(async (req, res, next) => {
  throw new Error('Something broke yet again! ')
}))

Сообщение об ошибке, выведенное в консоль

Написание собственного обработчика ошибок

Обработчики ошибок Express принимают 4 аргумента:

  1. error
  2. req
  3. res
  4. next

Размещать их нужно после промежуточных обработчиков и маршрутов.

app.use(/*...*/)
app.get(/*...*/)
app.post(/*...*/)
app.put(/*...*/)
app.delete(/*...*/)

// Собственный обработчик ошибок нужно поместить после всех остальных промежуточных обработчиков
app.use((error, req, res, next) => { /* ... */ })

Если создать собственный обработчик ошибок, то Express прекратит использование стандартного обработчика. Для того чтобы обработать ошибку, нужно сформировать ответ для фронтенд-приложения, которое обратилось к конечной точке, в которой возникла ошибка. Это означает, что нужно выполнить следующие действия:

  1. Сформировать и отправить подходящий код состояния ответа.
  2. Сформировать и отправить подходящий ответ.

То, какой именно код состояния подойдёт в каждом конкретном случае, зависит от того, что именно произошло. Вот список типичных ошибок, к обработке которых вы должны быть готовы:

  1. Ошибка 400 Bad Request. Используется в двух ситуациях. Во-первых — тогда, когда пользователь не включил в запрос необходимое поле (например — в отправленной платёжной форме не заполнено поле со сведениями о кредитной карте). Во-вторых — тогда, когда в запросе содержатся некорректные данные (например — ввод в поле пароля и в поле подтверждения пароля разных паролей).
  2. Ошибка 401 Unauthorized. Этот код состояния ответа применяется в том случае, если пользователь ввёл неправильные учётные данные (вроде имени пользователя, адреса электронной почты или пароля).
  3. Ошибка 403 Forbidden. Используется в тех случаях, когда пользователю не разрешён доступ к конечной точке.
  4. Ошибка 404 Not Found. Применяется в тех случаях, когда конечную точку невозможно обнаружить.
  5. Ошибка 500 Internal Server Error. Применяется тогда, когда запрос, отправленный фронтендом, сформирован правильно, но на бэкенде при этом возникла какая-то ошибка.

После того, как определён подходящий код состояния ответа, его нужно установить с помощью res.status:

app.use((error, req, res, next) => {
  // Ошибка, выдаваемая в ответ на неправильно сформированный запрос
  res.status(400)
  res.json(/* ... */)
})

Код состояния ответа должен соответствовать сообщению об ошибке. Для этого нужно отправлять код состояния вместе с ошибкой.

Легче всего это сделать с помощью пакета http-errors. Он позволяет отправлять в ошибке три фрагмента информации:

  1. Код состояния ответа.
  2. Сообщение, сопутствующее ошибке.
  3. Любые данные, которые нужно отправить (это необязательно).

Вот как установить пакет http-errors:

npm install http-errors --save

Вот как этим пакетом пользоваться:

const createError = require('http-errors')

// Создание ошибки
throw createError(status, message, properties)

Рассмотрим пример, который позволит как следует в этом всём разобраться.

Представим, что мы пытаемся обнаружить пользователя по адресу его электронной почты. Но этого пользователя найти не удаётся. В результате мы решаем отправить в ответ на соответствующий запрос ошибку User not found, сообщающую вызывающей стороне о том, что пользователь не найден.

Вот что нам нужно будет сделать при создании ошибки:

  1. Установить код состояния ответа как 400 Bad Request (ведь пользователь ввёл неправильные данные). Это будет наш первый параметр.
  2. Отправить вызывающей стороне сообщение наподобие User not found. Это будет второй параметр.

app.put('/testing', asyncHandler(async (req, res) => {
  const { email } = req.body
  const user = await User.findOne({ email })

  // Если пользователь не найден - выбросим ошибку
  if (!user) throw createError(400, `User '${email}' not found`)
}))

Получить код состояния можно с помощью конструкции error.status, а сообщение ошибки — с помощью error.message:

// Логирование ошибки
app.use((error, req, res, next) => {
  console.log('Error status: ', error.status)
  console.log('Message: ', error.message)
})

Результат логирования ошибки в консоли

Затем состояние ответа устанавливают с помощью res.status, а сообщение записывают в res.json:

app.use((error, req, res, next) => {
  // Установка кода состояния ответа
  res.status(error.status)

  // Отправка ответа
  res.json({ message: error.message })
})

Лично я предпочитаю отправлять в подобных ответах код состояния, сообщение и результат трассировки стека. Это облегчает отладку.

app.use((error, req, res, next) => {
  // Установка кода состояния ответа
  res.status(error.status)

  // Отправка ответа
  res.json({
    status: error.status,
    message: error.message,
    stack: error.stack
  })
})

▍Код состояния ответа, используемый по умолчанию

Если источником ошибки не является createError, то у неё не будет свойства status. Вот пример, в котором сделана попытка прочесть несуществующий файл с помощью fs.readFile:

const fs = require('fs')
const util = require('util')

// Преобразуем readFile из функции, использующей коллбэки, в async/await-функцию.
// Подробности об этом смотрите здесь: https://zellwk.com/blog/callbacks-to-promises
const readFilePromise = util.promisify(fs.readFile)

app.get('/testing', asyncHandler(async (req, res, next) => {
  const data = await readFilePromise('some-file')
})

У такого объекта ошибки не будет свойства status:

app.use((error, req, res, next) => {
  console.log('Error status: ', error.status)
  console.log('Message: ', error.message)
})

Результат логирования ошибки в консоли

В подобных случаях можно задать код ошибки, используемый по умолчанию. А именно, речь идёт об ошибке 500 Internal Server Error:

app.use((error, req, res, next) => {
  res.status(error.status || 500)
  res.json({
    status: error.status,
    message: error.message,
    stack: error.stack
  })
})

▍Изменение кода состояния ошибки

Предположим, мы собираемся прочитать некий файл, воспользовавшись данными, предоставленными пользователем. Если такого файла не существует, это значит, что нам нужно выдать ошибку 400 Bad Request. Ведь в том, что файл найти не удаётся, нет вины сервера.

В подобном случае нужно воспользоваться конструкцией try/catch для перехвата исходной ошибки. Затем нужно воссоздать объект ошибки с помощью createError:

app.get('/testing', asyncHandler(async (req, res, next) => {
  try {
    const { file } = req.body
    const contents = await readFilePromise(path.join(__dirname, file))
  } catch (error) {
    throw createError(400, `File ${file} does not exist`)
  }
})

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

Если запрос прошёл через все промежуточные обработчики и маршруты, но так и не был обработан, это означает, что конечная точка, соответствующая такому запросу, не была найдена.

Для обработки ошибок 404 Not Found нужно добавить, между маршрутами и обработчиком ошибок, дополнительный обработчик. Вот как выглядит создание объекта ошибки 404:

// Промежуточные обработчики...
// Маршруты...

app.use((req, res, next) => {
  next(createError(404))
})

// Обработчик ошибок...

Сведения об ошибке

▍Замечания об ошибке ERR_HTTP_HEADERS_SENT

Не впадайте в панику если видите сообщение об ошибке ERR_HTTP_HEADERS_SENT: Cannot set headers after they are sent to the client. Она возникает из-за того, что в одном и том же обработчике многократно вызывается метод, устанавливающий заголовки ответа. Вот методы, вызов которых приводит к автоматической установке заголовков ответа:

  1. res.send
  2. res.json
  3. res.render
  4. res.sendFile
  5. res.sendStatus
  6. res.end
  7. res.redirect

Так, например, если вы вызовете методы res.render и res.json в одном и том же обработчике ответа, то вы получите ошибку ERR_HTTP_HEADERS_SENT:

app.get('/testing', (req, res) => {
  res.render('new-page')
  res.json({ message: '¯_(ツ)_/¯' })
})

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

▍Обработка ошибок и потоковая передача данных

Если что-то идёт не так при потоковой передаче ответа фронтенду, то можно столкнуться с той же самой ошибкой ERR_HTTP_HEADERS_SENT.

В подобном случае обработку ошибок нужно передать стандартным обработчикам. Такой обработчик отправит ошибку и автоматически закроет соединение.

app.use((error, req, res, next) => {
  // Сделать это нужно только в том случае, если ответ передаётся в потоковом режиме
  if (res.headersSent) {
    return next(error)
  }

  // Остальной код обработки ошибок
})

Итоги

Сегодня я рассказал вам всё, что знаю об обработке ошибок в Express. Надеюсь, это поможет вам писать более надёжные Express-приложения.

Уважаемые читатели! Как вы обрабатываете ошибки в своих Node.js-проектах?

Обрабатывайте ошибки централизованно. Не в промежуточных слоях

Объяснение в один абзац

Без выделенного объекта для обработки ошибок есть больше шансов на то, что ошибки потеряются с радара из-за их неправильной обработки. Объект обработчика ошибок отвечает за отображение ошибки, например, путем записи в логгер, отправки событий в сервисы мониторинга, такие как Sentry, Rollbar или Raygun. Большинство веб-фреймворков, таких как Express, предоставляют механизм обработки ошибок с помощью функций промежуточной обработки (middlewares). Типичный поток обработки ошибок может выглядеть следующим образом: какой-то модуль выдает ошибку -> API-маршрутизатор перехватывает ошибку -> он передает ошибку функции промежуточной обработки (Express, KOA), которая отвечает за перехват ошибок -> вызывается централизованный обработчик ошибок -> функции промежуточной обработки передается информация о том, что является ли эта ошибка ненадежной (необрабатываемой), чтобы она могла корректно перезапустить приложение. Обратите внимание, что обычная, но неправильная практика — обрабатывать ошибки в функции промежуточной обработки Express — это не распространяется на ошибки, возникающие в не-веб-интерфейсах.

Пример кода — типичный поток ошибок

Javascript

// DAL-слой, мы не обрабатываем ошибки тут
DB.addDocument(newCustomer, (error, result) => {
  if (error)
    throw new Error('Great error explanation comes here', other useful parameters)
});

// код API-маршрутизатора, мы обрабатываем как sync
// так и async ошибки и переходим к middleware
try {
  customerService.addNew(req.body).then((result) => {
    res.status(200).json(result);
  }).catch((error) => {
    next(error)
  });
}
catch (error) {
  next(error);
}

// Обработка ошибок в middleware, мы делегируем обработку централизованному обработчику ошибок
app.use(async (err, req, res, next) => {
  const isOperationalError = await errorHandler.handleError(err);
  if (!isOperationalError) {
    next(err);
  }
});

Typescript

// DAL-слой, мы не обрабатываем ошибки тут
DB.addDocument(newCustomer, (error: Error, result: Result) => {
  if (error)
    throw new Error('Great error explanation comes here', other useful parameters)
});

// код API-маршрутизатора, мы обрабатываем как sync
// так и async ошибки и переходим к middleware
try {
  customerService.addNew(req.body).then((result: Result) => {
    res.status(200).json(result);
  }).catch((error: Error) => {
    next(error)
  });
}
catch (error) {
  next(error);
}

// Обработка ошибок в middleware, мы делегируем обработку централизованному обработчику ошибок
app.use(async (err: Error, req: Request, res: Response, next: NextFunction) => {
  const isOperationalError = await errorHandler.handleError(err);
  if (!isOperationalError) {
    next(err);
  }
});

Пример кода — обработка ошибок в выделенном объекте

Javascript

module.exports.handler = new errorHandler();

function errorHandler() {
  this.handleError = async (err) {
    await logger.logError(err);
    await sendMailToAdminIfCritical(err);
    await saveInOpsQueueIfCritical(err);
    await determineIfOperationalError(err);
  };
}

Typescript

class ErrorHandler {
  public async handleError(err: Error): Promise<void> {
    await logger.logError(err);
    await sendMailToAdminIfCritical(err);
    await saveInOpsQueueIfCritical(err);
    await determineIfOperationalError(err);
  };
}

export const handler = new ErrorHandler();

Пример кода — антипаттерн: обработка ошибок в middleware

Javascript

// middleware, обрабатывающий ошибки напрямую.
// А кто будет обрабатывать ошибки возникшие в Cron или при юнит-тестировании?
app.use((err, req, res, next) => {
  logger.logError(err);
  if (err.severity == errors.high) {
    mailer.sendMail(configuration.adminMail, 'Critical error occured', err);
  }
  if (!err.isOperational) {
    next(err);
  }
});

Typescript

// middleware, обрабатывающий ошибки напрямую.
// А кто будет обрабатывать ошибки возникшие в Cron или при юнит-тестировании?
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  logger.logError(err);
  if (err.severity == errors.high) {
    mailer.sendMail(configuration.adminMail, 'Critical error occured', err);
  }
  if (!err.isOperational) {
    next(err);
  }
});

Цитата из блога: «Иногда нижние слои не могут сделать ничего полезного, кроме как сообщить об ошибке вызывающему слою»

Из блога Joyent, занимающего 1 место по ключевым словам «Обработка ошибок Node.js»

… Вы можете обработать одну и ту же ошибку на нескольких слоях. Это происходит, когда нижние слои не могут сделать ничего полезного, кроме как передать ошибку вызывающему слою, который передаст ошибку своему вызывающему слою, и так далее. Зачастую только самый верхний слой знает, что является подходящим действием на ошибку: попытка повторить операцию, сообщить пользователю об ошибке или что-то еще. Но это не значит, что вы должны пытаться сообщать обо всех ошибках в один верхний callback, потому что этот callback не может знать, в каком контексте произошла ошибка …

Цитата из блога: «Обработка каждой ошибки по отдельности приведет к ужасному дублированию»

Из блога JS Recipes, занимающего 17 место по ключевым словам «Обработка ошибок Node.js»

… Только в контроллере api.js Hackathon Starter имеется более 79 объектов ошибок. Обработка каждой ошибки в отдельности привела бы к ужасному дублированию кода. Следующее, что вы можете сделать, это делегировать всю логику обработки ошибок в middleware Express …

Цитата из блога: «В коде вашей базы данных нет места ошибкам HTTP»

Из блога Daily JS, занимающем 14 место по ключевым словам «Обработка ошибок Node.js»

… Вы должны добавлять полезные свойства в объекты ошибок, но использовать их согласовано. И не пересекайте логику: в коде вашей базы данных нет места ошибкам HTTP. Или, например, для frontend-разработчиков, ошибки Ajax имеют место в коде, который общается с сервером, но не в коде, который работает с шаблонами Mustache …

I just recently started working on an express.js based application, which also uses the pg module (https://github.com/brianc/node-postgres)

I also spent a significant amount of time, reading about node and express approach error handling, the benefits of properly designing middleware, etc. Yet, a recurring problem is still buzzing me without a solution.

Say, I have the following router method:

app.get("/:someThing/:someId", function(req, res, next) {
  pgClient.query("some SQL query", function(err, data) {
    if (err) { return next(err); } // some 500 handler will take it
    if (data.rows.length == 0) {
      next(); // send it over to a 404 handler
    }

    //finally, here we get the chance to do something with the data.
    //and send it over via res.json or something else
  });
});

If I’ve read correctly, this should be the proper way to do it. Yet, I bet you can also admt that it is too much of boilerplate to rewrite over and over … and over again, even in the very same router method, in case we have multiple nested callbacks.

I’ve been asking myself what the best way to handle such a situation centrally would be. All of my ideas involve intercepting the pgClient.query method. In one, the query method will simply throw the error instead of passing it to the callback. In another, the call to the pgClient.query will send the router method’s next to pgClient. Then the intercepted query method will know how to deal with the next being passed to it.

From what I know, throwing errors around is not really the appropriate way to get it to the 500 handlers. On another hand, passin next as an option to pgClient, gives such a low level a lot of knowledge about the the layers above, which based on my knowledge and experience, can lead to coupling, and is not very good either.

What do you suggest?

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

Функции промежуточного обработчика для обработки ошибок определяются так же, как и другие функции промежуточной обработки, но с указанием для функции обработки ошибок не трех, а четырех аргументов: (err, req, res, next). Например:


app.use(function(err, req, res, next) {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

Промежуточный обработчик для обработки ошибок должен быть определен последним, после указания всех app.use() и вызовов маршрутов; например:


var bodyParser = require('body-parser');
var methodOverride = require('method-override');

app.use(bodyParser());
app.use(methodOverride());
app.use(function(err, req, res, next) {
  // logic
});

Ответы, поступающие из функции промежуточной обработки, могут иметь любой формат, в зависимости от ваших предпочтений. Например, это может быть страница сообщения об ошибке HTML, простое сообщение или строка JSON.

В целях упорядочения (и для фреймворков более высокого уровня) можно определить несколько функций промежуточной обработки ошибок, точно так же, как это допускается для обычных функций промежуточной обработки. Например, для того чтобы определить обработчик ошибок для запросов, совершаемых с помощью XHR, и для остальных запросов, можно воспользоваться следующими командами:


var bodyParser = require('body-parser');
var methodOverride = require('method-override');

app.use(bodyParser());
app.use(methodOverride());
app.use(logErrors);
app.use(clientErrorHandler);
app.use(errorHandler);

В данном примере базовый код logErrors может записывать информацию о запросах и ошибках в stderr, например:


function logErrors(err, req, res, next) {
  console.error(err.stack);
  next(err);
}

Кроме того, в данном примере clientErrorHandler определен, как указано ниже; в таком случае ошибка явным образом передается далее следующему обработчику:


function clientErrorHandler(err, req, res, next) {
  if (req.xhr) {
    res.status(500).send({ error: 'Something failed!' });
  } else {
    next(err);
  }
}

“Обобщающая” функция errorHandler может быть реализована так:


function errorHandler(err, req, res, next) {
  res.status(500);
  res.render('error', { error: err });
}

При передаче какого-либо объекта в функцию next() (кроме строки 'route'), Express интерпретирует текущий запрос как ошибку и пропустит все остальные функции маршрутизации и промежуточной обработки, не являющиеся функциями обработки ошибок. Для того чтобы обработать данную ошибку определенным образом, необходимо создать маршрут обработки ошибок, как описано в следующем разделе.

Если задан обработчик ошибок с несколькими функциями обратного вызова, можно воспользоваться параметром route, чтобы перейти к следующему обработчику маршрута. Например:


app.get('/a_route_behind_paywall',
  function checkIfPaidSubscriber(req, res, next) {
    if(!req.user.hasPaid) {

      // continue handling this request
      next('route');
    }
  }, function getPaidContent(req, res, next) {
    PaidContent.find(function(err, doc) {
      if(err) return next(err);
      res.json(doc);
    });
  });

В данном примере обработчик getPaidContent будет пропущен, но выполнение всех остальных обработчиков в app для /a_route_behind_paywall будет продолжено.

Вызовы next() и next(err) указывают на завершение выполнения текущего обработчика и на его состояние. next(err) пропускает все остальные обработчики в цепочке, кроме заданных для обработки ошибок, как описано выше.

Стандартный обработчик ошибок

В Express предусмотрен встроенный обработчик ошибок, который обрабатывает любые возможные ошибки, встречающиеся в приложении. Этот стандартный обработчик ошибок добавляется в конец стека функций промежуточной обработки.

В случае передачи ошибки в next() без обработки с помощью обработчика ошибок, такая ошибка будет обработана встроенным обработчиком ошибок. Ошибка будет записана на клиенте с помощью трассировки стека. Трассировка стека не включена в рабочую среду.

Для запуска приложения в рабочем режиме необходимо задать для переменной среды NODE_ENV значение production.

При вызове next() с ошибкой после начала записи ответа
(например, если ошибка обнаружена во время включения ответа в поток, направляемый клиенту), стандартный обработчик ошибок Express закрывает соединение и отклоняет запрос.

Поэтому при добавлении нестандартного обработчика ошибок вам потребуется делегирование в стандартные
механизмы обработки ошибок в Express в случае, если заголовки уже были отправлены клиенту:


function errorHandler(err, req, res, next) {
  if (res.headersSent) {
    return next(err);
  }
  res.status(500);
  res.render('error', { error: err });
}

Понравилась статья? Поделить с друзьями:
  • Ценные сокровища лексическая ошибка
  • Централизованная обработка ошибок express
  • Цена первой ошибки бондаренко читать
  • Центр обновления ошибка 0x80070002
  • Цена ошибок крушение