Обработка ошибок
Функции промежуточного обработчика для обработки ошибок определяются так же, как и другие функции промежуточной обработки, но с указанием для функции обработки ошибок не трех, а четырех аргументов: (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. Мы собираемся объяснить, что такое ошибка, различные типы ошибок, которые могут возникнуть, и как их обрабатывать в нашем приложении. Вот некоторые вещи, которыми мы займемся в следующих главах:
- узнаем, что на самом деле такое «обработка ошибок» и с какими типами ошибок вы столкнетесь
- узнаем об объекте Node.js
Error
и о том, как его использовать - узнаем, как создавать собственные классы ошибок и как они могут помочь нам в разработке лучших API-интерфейсов и Node-приложений
- узнаем о промежуточном программном обеспечении Express и о том, как его использовать для обработки наших ошибок
- научимся структурировать информацию об ошибках и представлять ее потребителю и разработчику
Предусловие
Предупреждение! В этой статье предполагается, что вы уже кое-что знаете. Несмотря на то, что это удобно для новичков, вот что вам следует знать, чтобы извлечь из этой статьи максимальную пользу:
- рабочее знание Node.js
- рабочее знание Express.js (маршруты, промежуточное ПО и т. д.)
- основы TypeScript (и классы!)
- основы работы API и его написания с использованием Express.js
Хорошо. Мы можем начинать.
Что такое обработка ошибок и зачем она вам нужна?
Так что же такое «обработка ошибок» на самом деле?
Обработка ошибок (или обработка исключений) — это процесс реагирования на возникновение ошибок (аномальное/нежелательное поведение) во время выполнения программы.
Зачем нужна обработка ошибок?
Потому что мы хотим сделать исправление ошибок менее болезненным. Это также помогает нам писать более чистый код, поскольку весь код обработки ошибок централизован, вместо того, чтобы обрабатывать ошибки там, где мы думаем, что они могут возникнуть. В конце концов — код более организован, вы меньше повторяетесь, и это сокращает время разработки и обслуживания.
Типы ошибок
Есть два основных типа ошибок, которые нам необходимо различать и соответственно обрабатывать.
Операционные ошибки
Операционные ошибки представляют собой проблемы во время выполнения. Это не обязательно «ошибки», это внешние обстоятельства, которые могут нарушить ход выполнения программы. Несмотря на то, что они не являются ошибками в вашем коде, эти ситуации могут (и неизбежно будут) возникать, и с ними нужно обращаться. Вот некоторые примеры:
- Запрос API не выполняется по какой-либо причине (например, сервер не работает или превышен лимит скорости)
- Невозможно установить соединение с базой данных
- Пользователь отправляет неверные входные данные
- Системе не хватает памяти
Ошибки программиста
Ошибки программиста — это настоящие «ошибки», поэтому они представляют собой проблемы в самом коде. Как ошибки в синтаксисе или логике программы, они могут быть устранены только путем изменения исходного кода. Вот несколько примеров ошибок программиста:
- Попытка прочитать свойство объекта, которое не определено
- Передача некорректных параметров в функции
- не улавливая отвергнутого обещания
Что такое ошибка узла?
В Node.js есть встроенный объект Error
который мы будем использовать в качестве основы для выдачи ошибок. При вызове он содержит набор информации, которая сообщает нам, где произошла ошибка, тип ошибки и в чем проблема. В документации Node.js есть более подробное объяснение.
Мы можем создать такую ошибку:
const error = new Error('Error message');
Итак, мы дали ему строковый параметр, который будет сообщением об ошибке. Но что еще есть в этом Error
? Поскольку мы используем typescript, мы можем проверить его определение, что приведет нас к interface
typescript:
const error = new Error('Error message');
Name
и message
не требуют пояснений, а stack
содержит name
, message
и строку, описывающую точку в коде, в которой был создан 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:
- ValidationError (ошибки, которые можно использовать при обработке входящих пользовательских данных)
- DatabaseError (ошибки, которые вы можете использовать, чтобы сообщить пользователю, что существует проблема с взаимодействием с базой данных)
- 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, требующего большего от ваших навыков. Эти концепции очень важны, и с ними легко начать, и я надеюсь, что вам понравилась моя статья и вы узнали что-то новое.
Обрабатывайте ошибки централизованно. Не в промежуточных слоях
Объяснение в один абзац
Без выделенного объекта для обработки ошибок есть больше шансов на то, что ошибки потеряются с радара из-за их неправильной обработки. Объект обработчика ошибок отвечает за отображение ошибки, например, путем записи в логгер, отправки событий в сервисы мониторинга, такие как 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 …
Improve Article
Save Article
Improve Article
Save Article
Error handling in Express is referred to as something that handles or processes errors that may come while executing any synchronous code or asynchronous code.
What do we mean by synchronous or asynchronous code?
A lot of times, it happens that an operation begins executing but for some reason faces some delay before completion. The common examples of such operations are HTTP requests such as AJAX requests, functions such as setTimeout etc. These operations begin, and then end when the response returns or when the timer ends. While the computer waits for these operations to complete, it moves on busying itself with the next lines of code. It keeps busy, but this causes a significant challenge — any code dependent on previous asynchronous code might be run before that asynchronous code is complete, meaning errors. Have a look below-
var
data = makeAsyncRequest();
console.log(
"Data is "
+ data);
We can say that, when we execute something synchronously, we wait for it to finish before moving on another task. When we execute something asynchronously, we can move on to another task before it finishes. The above problem is solved using callbacks, middleware modules or by using the modern promise and await.
Catching Errors in Express
- If synchronous code having route handlers and middleware throws any error, then without any effort and extra work, Express solves it by catching and processing the error without requiring any of our consent. Have a look at the below code –
app.get(
'/'
,
function
(req, res) {
throw
new
Error(
'Died'
)
})
- If a route handler and middleware invokes asynchronous function which in turn produces some errors, then we have to explicitly pass the error to the next() function, where Express will catch and process them. The following illustration will help you to understand
app.get(
'/'
,
function
(req, res, next) {
fs.readFile(
'/file-is-not-available'
,
function
(err, data) {
if
(err) {
next(err)
}
else
{
res.send(data)
}
})
})
- Route handlers and middlewares that return a Promise will call next(value) automatically when they reject or throw an error.
app.get(
'/user/:id'
, async
function
(req, res, next) {
var
user = await getUserById(req.params.id)
res.send(user)
})
The await keyword can be used to indicate that the function that follows will be returning a promise, which should be awaited before executing any other dependent code. ‘await’ can only be used inside an async function.
Next (next) will be called with either the thrown error or the rejected value in case if getUserById throws an error or rejects. If no rejected value is provided, next will be called with a default Error object provided by the Express router. If we pass anything to the next() function (except the string ‘route’), Express regards the current request as being an error and will skip any remaining non-error handling routing and middleware functions. - If a given callback in a sequence provides no data and only errors, then the code can be simplified as –
app.get(
'/'
, [
function
(req, res, next) {
fs.writeFile(
'/path-cannot-be-accessed'
,
'data'
, next)
},
function
(req, res) {
res.send(
'OK'
)
}
])
In the above code, next is provided as the callback which runs without caring whether errors comes out or not. If there is no error, then the second handler also runs otherwise express just catches and processes the error.
Now, look at the example below –
app.get(
'/'
,
function
(req, res, next) {
setTimeout(
function
() {
try
{
throw
new
Error(
'Died'
)
}
catch
(err) {
next(err)
}
}, 100)
})
- As we know, if a route handler and middleware invokes an asynchronous function which in turn produces some errors, then we have to explicitly pass the error to the next() function, where Express will catch and process them. However, in the above code, the error is not the part of the synchronous code, so we can’t simply pass it to the next function. We need to first throw the errors, catch those errors generated by asynchronous code, and then pass it to the Express. For this, we need to use the try..catch block to catch them. If you don’t want to use try and catch, then simply use promises as shown below –
app.get(
'/'
,
function
(req, res, next) {
Promise.resolve().then(
function
() {
throw
new
Error(
'Died'
)
}).
catch
(next)
})
Since promises automatically catch both synchronous errors and rejected promises, you can simply provide next as the final catch handler and Express will catch errors, because the catch handler is given the error as the first argument.
- Default Error Handlers: The default error handler catches the error when we call next and don’t handle it with a custom error handler. If we want to send a different response to the default, we have to write our own error handler. This default error-handling middleware function is added at the end of the middleware function stack. If you pass an error to next() and you do not handle it in a custom error handler, it will be handled by the built-in error handler, the error will be written to the client with the stack trace. The stack trace is not included in the production environment.
When an error is written, the following information is added automatically to the response:
- The res.statusCode is set from err.status (or err.statusCode). If this value is outside the 4xx or 5xx range, it will be set to 500.
- The res.statusMessage is set as per the status code.
- The body will be the HTML of the status code message when in production environment, otherwise will be err.stack.
- Any headers specified in an err.headers object.
If you call next() with an error after you have started writing the response (for example, if you encounter an error while streaming the response to the client) the Express default error handler closes the connection and fails the request.
So when you add a custom error handler, you must delegate to the default Express error handler, when the headers have already been sent to the client:
function
errorHandler (err, req, res, next) {
if
(res.headersSent) {
return
next(err)
}
res.status(500)
res.render(
'error'
, { error: err })
}
Note that the default error handler can get triggered if you call next() with an error in your code more than once, even if custom error handling middleware is in place.
How to write Error handlers?
The way we declare the middleware functions, in the same way, error handling functions are defined. However, error-handling functions have four arguments instead of three: (err, req, res, next). For example –
app.use(
function
(err, req, res, next) {
console.error(err.stack)
res.status(500).send(
'Something broke!'
)
})
We need to define error-handling middleware last, after other app.use() and routes calls. The example is shown below –
app.get(
'/'
, (req, res, next) => {
req.foo =
true
;
setTimeout(() => {
try
{
throw
new
Error(
'error'
);
}
catch
(ex) {
next(ex);
}
})
});
app.use((err, req, res, next) => {
if
(req.foo) {
res.status(500).send(
'Fail!'
);
}
else
{
next(err);
}
})
app.use((err, req, res, next) => {
res.status(500).send(
'Error!'
)
})
Improve Article
Save Article
Improve Article
Save Article
Error handling in Express is referred to as something that handles or processes errors that may come while executing any synchronous code or asynchronous code.
What do we mean by synchronous or asynchronous code?
A lot of times, it happens that an operation begins executing but for some reason faces some delay before completion. The common examples of such operations are HTTP requests such as AJAX requests, functions such as setTimeout etc. These operations begin, and then end when the response returns or when the timer ends. While the computer waits for these operations to complete, it moves on busying itself with the next lines of code. It keeps busy, but this causes a significant challenge — any code dependent on previous asynchronous code might be run before that asynchronous code is complete, meaning errors. Have a look below-
var
data = makeAsyncRequest();
console.log(
"Data is "
+ data);
We can say that, when we execute something synchronously, we wait for it to finish before moving on another task. When we execute something asynchronously, we can move on to another task before it finishes. The above problem is solved using callbacks, middleware modules or by using the modern promise and await.
Catching Errors in Express
- If synchronous code having route handlers and middleware throws any error, then without any effort and extra work, Express solves it by catching and processing the error without requiring any of our consent. Have a look at the below code –
app.get(
'/'
,
function
(req, res) {
throw
new
Error(
'Died'
)
})
- If a route handler and middleware invokes asynchronous function which in turn produces some errors, then we have to explicitly pass the error to the next() function, where Express will catch and process them. The following illustration will help you to understand
app.get(
'/'
,
function
(req, res, next) {
fs.readFile(
'/file-is-not-available'
,
function
(err, data) {
if
(err) {
next(err)
}
else
{
res.send(data)
}
})
})
- Route handlers and middlewares that return a Promise will call next(value) automatically when they reject or throw an error.
app.get(
'/user/:id'
, async
function
(req, res, next) {
var
user = await getUserById(req.params.id)
res.send(user)
})
The await keyword can be used to indicate that the function that follows will be returning a promise, which should be awaited before executing any other dependent code. ‘await’ can only be used inside an async function.
Next (next) will be called with either the thrown error or the rejected value in case if getUserById throws an error or rejects. If no rejected value is provided, next will be called with a default Error object provided by the Express router. If we pass anything to the next() function (except the string ‘route’), Express regards the current request as being an error and will skip any remaining non-error handling routing and middleware functions. - If a given callback in a sequence provides no data and only errors, then the code can be simplified as –
app.get(
'/'
, [
function
(req, res, next) {
fs.writeFile(
'/path-cannot-be-accessed'
,
'data'
, next)
},
function
(req, res) {
res.send(
'OK'
)
}
])
In the above code, next is provided as the callback which runs without caring whether errors comes out or not. If there is no error, then the second handler also runs otherwise express just catches and processes the error.
Now, look at the example below –
app.get(
'/'
,
function
(req, res, next) {
setTimeout(
function
() {
try
{
throw
new
Error(
'Died'
)
}
catch
(err) {
next(err)
}
}, 100)
})
- As we know, if a route handler and middleware invokes an asynchronous function which in turn produces some errors, then we have to explicitly pass the error to the next() function, where Express will catch and process them. However, in the above code, the error is not the part of the synchronous code, so we can’t simply pass it to the next function. We need to first throw the errors, catch those errors generated by asynchronous code, and then pass it to the Express. For this, we need to use the try..catch block to catch them. If you don’t want to use try and catch, then simply use promises as shown below –
app.get(
'/'
,
function
(req, res, next) {
Promise.resolve().then(
function
() {
throw
new
Error(
'Died'
)
}).
catch
(next)
})
Since promises automatically catch both synchronous errors and rejected promises, you can simply provide next as the final catch handler and Express will catch errors, because the catch handler is given the error as the first argument.
- Default Error Handlers: The default error handler catches the error when we call next and don’t handle it with a custom error handler. If we want to send a different response to the default, we have to write our own error handler. This default error-handling middleware function is added at the end of the middleware function stack. If you pass an error to next() and you do not handle it in a custom error handler, it will be handled by the built-in error handler, the error will be written to the client with the stack trace. The stack trace is not included in the production environment.
When an error is written, the following information is added automatically to the response:
- The res.statusCode is set from err.status (or err.statusCode). If this value is outside the 4xx or 5xx range, it will be set to 500.
- The res.statusMessage is set as per the status code.
- The body will be the HTML of the status code message when in production environment, otherwise will be err.stack.
- Any headers specified in an err.headers object.
If you call next() with an error after you have started writing the response (for example, if you encounter an error while streaming the response to the client) the Express default error handler closes the connection and fails the request.
So when you add a custom error handler, you must delegate to the default Express error handler, when the headers have already been sent to the client:
function
errorHandler (err, req, res, next) {
if
(res.headersSent) {
return
next(err)
}
res.status(500)
res.render(
'error'
, { error: err })
}
Note that the default error handler can get triggered if you call next() with an error in your code more than once, even if custom error handling middleware is in place.
How to write Error handlers?
The way we declare the middleware functions, in the same way, error handling functions are defined. However, error-handling functions have four arguments instead of three: (err, req, res, next). For example –
app.use(
function
(err, req, res, next) {
console.error(err.stack)
res.status(500).send(
'Something broke!'
)
})
We need to define error-handling middleware last, after other app.use() and routes calls. The example is shown below –
app.get(
'/'
, (req, res, next) => {
req.foo =
true
;
setTimeout(() => {
try
{
throw
new
Error(
'error'
);
}
catch
(ex) {
next(ex);
}
})
});
app.use((err, req, res, next) => {
if
(req.foo) {
res.status(500).send(
'Fail!'
);
}
else
{
next(err);
}
})
app.use((err, req, res, next) => {
res.status(500).send(
'Error!'
)
})
Обработка ошибок¶
Функции промежуточного обработчика для обработки ошибок определяются так же, как и другие функции промежуточной обработки, но с указанием для функции обработки ошибок не трех, а четырех аргументов: (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 })
}