Ниже приводится краткое изложение различных источников по этой теме, включая пример кода и цитаты из отдельных сообщений в блоге. Полный список лучших практик можно найти здесь
Лучшие практики обработки ошибок Node.JS
Number1: использовать обещания для обработки асинхронных ошибок
TL; DR: Обработка асинхронных ошибок в стиле обратного вызова, вероятно, является самым быстрым путем в ад (a.k.a пирамида гибели). Лучший подарок, который вы можете дать своему коду, - это использовать надежную библиотеку обещаний, которая обеспечивает очень компактный и знакомый синтаксис кода, такой как try-catch
В противном случае: Стиль обратного вызова Node.JS, функция (err, response), является многообещающим способом непригодного для использования кода из-за сочетания обработки ошибок со случайным кодом, чрезмерного вложения и неуклюжего кодирования.
Пример кода - хорошо
doWork()
.then(doWork)
.then(doError)
.then(doWork)
.catch(errorHandler)
.then(verify);
пример кода, анти-шаблон - обработка ошибок в стиле обратного вызова
getData(someParameter, function(err, result){
if(err != null)
//do something like calling the given callback function and pass the error
getMoreData(a, function(err, result){
if(err != null)
//do something like calling the given callback function and pass the error
getMoreData(b, function(c){
getMoreData(d, function(e){
...
});
});
});
});
});
Цитата блога: "У нас проблема с обещаниями"
(Из блога pouchdb занял 11 место по ключевым словам "Узловые обещания")
"... На самом деле, обратные вызовы делают что-то еще более зловещее: они лишают нас стека, что мы обычно принимаем как должное в языках программирования. Написание кода без стека во многом похоже на вождение автомобиля без тормоза педаль: вы не понимаете, насколько сильно вам это нужно, пока вы не дотянетесь до него, а его там нет. Весь смысл обещаний состоит в том, чтобы вернуть нам языковые основы, которые мы потеряли, когда мы стали асинхронными: вернуть, выбросить, и стек. Но вы должны знать, как правильно использовать обещания, чтобы воспользоваться ими."
Number2: использовать только встроенный объект Error
TL; DR: Довольно часто можно увидеть код, который выдает ошибки в виде строки или пользовательского типа - это усложняет логику обработки ошибок и взаимодействие между модулями. Вне зависимости от того, отклоняете ли вы обещание, генерируете исключение или отправляете ошибку - использование встроенного в объект Node.JS Error повышает единообразие и предотвращает потерю информации об ошибках
В противном случае: При выполнении какого-либо модуля, будучи неуверенным, какой тип ошибок приходит в ответ - намного сложнее рассуждать о наступающем исключении и обрабатывать его. Даже лучше, использование пользовательских типов для описания ошибок может привести к потере информации о критических ошибках, таких как трассировка стека!
Пример кода - все правильно
//throwing an Error from typical function, whether sync or async
if(!productToAdd)
throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promise
return new promise(function (resolve, reject) {
DAL.getProduct(productToAdd.id).then((existingProduct) =>{
if(existingProduct != null)
return reject(new Error("Why fooling us and trying to add an existing product?"));
пример кода анти-паттерн
//throwing a String lacks any stack trace information and other important properties
if(!productToAdd)
throw ("How can I add new product when no value provided?");
Цитата блога: "Строка не является ошибкой"
(Из блога devthought занял 6 место по ключевым словам «Node.JS error object»)
"… передача строки вместо ошибки приводит к снижению совместимости между модулями. Это нарушает контракты с API, которые могут выполнять экземпляр проверки ошибок или которые хотят узнать больше об ошибке . Объекты ошибок как мы увидим, у современных движков JavaScript есть очень интересные свойства, помимо хранения сообщения, переданного конструктору .. "
Number3: различить операционные и программистские ошибки
TL; DR: Ошибки операций (например, API получил неверный ввод) относятся к известным случаям, когда влияние ошибки полностью осознается и может быть обработано вдумчиво. С другой стороны, ошибка программиста (например, попытка прочитать неопределенную переменную) относится к неизвестным ошибкам кода, которые требуют изящного перезапуска приложения
В противном случае: При возникновении ошибки вы всегда можете перезапустить приложение, но зачем подводить ~ 5000 онлайн-пользователей из-за незначительной и прогнозируемой ошибки (ошибка в работе)? обратное также не идеально - поддержание приложения в случае возникновения неизвестной проблемы (ошибка программиста) может привести к непредсказуемому поведению. Разграничение между ними позволяет действовать тактично и применять сбалансированный подход, основанный на данном контексте
Пример кода - все правильно
//throwing an Error from typical function, whether sync or async
if(!productToAdd)
throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promise
return new promise(function (resolve, reject) {
DAL.getProduct(productToAdd.id).then((existingProduct) =>{
if(existingProduct != null)
return reject(new Error("Why fooling us and trying to add an existing product?"));
пример кода - пометка ошибки как рабочей (доверенной)
//marking an error object as operational
var myError = new Error("How can I add new product when no value provided?");
myError.isOperational = true;
//or if you're using some centralized error factory (see other examples at the bullet "Use only the built-in Error object")
function appError(commonType, description, isOperational) {
Error.call(this);
Error.captureStackTrace(this);
this.commonType = commonType;
this.description = description;
this.isOperational = isOperational;
};
throw new appError(errorManagement.commonErrors.InvalidInput, "Describe here what happened", true);
//error handling code within middleware
process.on('uncaughtException', function(error) {
if(!error.isOperational)
process.exit(1);
});
Блог Цитата : «В противном случае вы рискуете государством»
(Из отлаживаемого блога занял 3 место по ключевым словам "Node.JS uncaught исключения")
"… По самой природе того, как throw работает в JavaScript, почти никогда не существует способа безопасно« взять то, что вы остановили », без утечек ссылок или создания какого-либо другого неопределенного хрупкого состояния. Самый безопасный способ отреагировать на возникшую ошибку - завершить процесс . Конечно, на обычном веб-сервере у вас может быть открыто много подключений, и нецелесообразно внезапно закрывать их, потому что произошла ошибка кем-то другим. Лучший подход состоит в том, чтобы отправить ответ об ошибке на запрос, который вызвал ошибку, позволяя остальным завершить работу в обычное время и прекратить прослушивать новые запросы в этом работнике "
Number4: обрабатывать ошибки централизованно, но не в промежуточном программном обеспечении
TL; DR: Логика обработки ошибок, такая как почта для администратора и ведение журнала, должна быть заключена в выделенный и централизованный объект, который вызывают все конечные точки (например, промежуточное программное обеспечение Express, задания cron, модульное тестирование) когда появляется ошибка.
В противном случае: Если не обрабатывать ошибки в одном месте, это приведет к дублированию кода и, вероятно, к ошибкам, которые обрабатываются неправильно
Пример кода - типичный поток ошибок
//DAL layer, we don't handle errors here
DB.addDocument(newCustomer, (error, result) => {
if (error)
throw new Error("Great error explanation comes here", other useful parameters)
});
//API route code, we catch both sync and async errors and forward to the middleware
try {
customerService.addNew(req.body).then(function (result) {
res.status(200).json(result);
}).catch((error) => {
next(error)
});
}
catch (error) {
next(error);
}
//Error handling middleware, we delegate the handling to the centrzlied error handler
app.use(function (err, req, res, next) {
errorHandler.handleError(err).then((isOperationalError) => {
if (!isOperationalError)
next(err);
});
});
Цитата из блога:"Иногда нижние уровни не могут сделать ничего полезного, кроме как сообщить об ошибке своему вызывающему"
(Из блога Joyent занял 1 место по ключевым словам «Обработка ошибок Node.JS»)
"… В конечном итоге вы можете обработать одну и ту же ошибку на нескольких уровнях стека. Это происходит, когда нижние уровни не могут сделать ничего полезного, кроме как передать ошибку своему вызывающему, который передает ошибку своему вызывающему, и т. Д. Зачастую только вызывающий пользователь верхнего уровня знает, что является подходящим ответом, будь то попытка повторить операцию, сообщить об ошибке пользователю или что-то еще. Но это не значит, что вы должны пытаться сообщать обо всех ошибках одному обратный вызов верхнего уровня, поскольку сам этот обратный вызов не может знать, в каком контексте произошла ошибка "
Number5: ошибки API документа при использовании Swagger
TL; DR: Пусть ваши вызывающие API знают, какие ошибки могут появиться в ответ, чтобы они могли обрабатывать их вдумчиво, без сбоев. Обычно это делается с помощью каркасов документации REST API, таких как Swagger
В противном случае: Клиент API может принять решение о сбое и перезапуске только потому, что он получил ошибку, которую он не мог понять. Примечание: вызывающим абонентом вашего API может быть вы (очень типично для среды микросервисов)
Цитата блога: «Вы должны сообщить своим абонентам, какие ошибки могут произойти»
(Из блога Joyent занял 1 место по ключевым словам «Регистрация Node.JS»)
… Мы говорили о том, как обрабатывать ошибки, но когда вы пишете новую функцию, как вы доставляете ошибки в код, вызвавший вашу функцию? … Если вы не знаете, какие ошибки могут произойти, или не знаете, что они означают, то ваша программа может быть исправлена только случайно. Поэтому, если вы пишете новую функцию, вы должны сообщить своим абонентам, какие ошибки могут произойти и что они имеют значение
Number6: Изящно завершите процесс, когда в город придет незнакомец
TL; DR: Когда неизвестен ошибочныйили происходит (ошибка разработчика, см. рекомендацию № 3) - существует неопределенность относительно работоспособности приложения.Обычная практика предлагает осторожно перезапустить процесс с помощью инструмента «перезапуска», такого как Forever и PM2
В противном случае: При обнаружении незнакомого исключения некоторый объект может быть в неисправном состоянии (например,генератор событий, который используется глобально и больше не запускает события из-за некоторого внутреннего сбоя), и все будущие запросы могут завершаться сбоем или вести себя безумно
Пример кода - решение о сбое
//deciding whether to crash when an uncaught exception arrives
//Assuming developers mark known operational errors with error.isOperational=true, read best practice #3
process.on('uncaughtException', function(error) {
errorManagement.handler.handleError(error);
if(!errorManagement.handler.isTrustedError(error))
process.exit(1)
});
//centralized error handler encapsulates error-handling related logic
function errorHandler(){
this.handleError = function (error) {
return logger.logError(err).then(sendMailToAdminIfCritical).then(saveInOpsQueueIfCritical).then(determineIfOperationalError);
}
this.isTrustedError = function(error)
{
return error.isOperational;
}
Цитата блога: «Есть три школы мысли об обработке ошибок» (Из блога jsrecipes)
… Существует три школы мысли об обработке ошибок: 1. Пусть приложение аварийно завершает работу и перезапускает его.2. Обработка всех возможных ошибок и никогда не сбои. 3.Сбалансированный подход между двумя
Number7: использование расширенного регистратора для увеличения видимости ошибок
TL; DR: набор зрелыхинструменты журналирования, такие как Winston, Bunyan или Log4J, ускорят обнаружение и понимание ошибок.Так что забудьте о console.log.
В противном случае: Сканирование в файлах console.logs или вручную через грязный текстовый файл без запросов инструментов или приличного средства просмотра журналов может занять вас на работе допоздна
Пример кода - регистратор Winston в действии
//your centralized logger object
var logger = new winston.Logger({
level: 'info',
transports: [
new (winston.transports.Console)(),
new (winston.transports.File)({ filename: 'somefile.log' })
]
});
//custom code somewhere using the logger
logger.log('info', 'Test Log Message with some parameter %s', 'some parameter', { anything: 'This is metadata' });
Цитата блога:"Позволяет определить несколько требований (для регистратора):" (Отблог strongblog)
… Позволяет определить несколько требований (для регистратора): 1. Отметка времени каждой строки журнала.Это довольно очевидно - вы должны быть в состоянии сказать, когда произошла каждая запись в журнале.2. Формат регистрации должен быть легко усваиваемым людьми, а также машинами.3. Позволяет для нескольких настраиваемых потоков назначения.Например, вы можете записывать журналы трассировки в один файл, но при возникновении ошибки запишите в тот же файл, затем в файл ошибок и одновременно отправьте электронное письмо…
Number8: Обнаружение ошибок и простоев с использованием продуктов APM
TL; DR: Продукты для мониторинга и производительности (также известные как APM) заранее измеряют вашу кодовую базу или API, чтобы они могли автоматически подсвечивать ошибки, сбои имедленные части, которые вы пропустили
В противном случае: Вы можете потратить огромные усилия на измерение производительности и времени простоя API, возможно, вы никогда не узнаете, какие части кода медленнее всего работают в реальных условиях, икак это влияет на UX
Цитата блога:"Сегменты продуктов APM" (из блога Йони Голдберг)
"… Продукты APM составляют 3 основных сегмента: 1. Мониторинг веб-сайтов или API - внешние сервисы, которые постоянно отслеживают время безотказной работы и производительность посредством HTTP-запросов. Может быть настроено за считанные минутыes. Ниже приведены несколько выбранных претендентов: Pingdom, Uptime Robot и New Relic 2.Инструментарий кода - семейство продуктов , для которых требуется встроить агент в приложение, чтобы обеспечить медленное обнаружение кода, статистику исключений, мониторинг производительности и многое другое.Ниже приведены несколько выбранных претендентов: New Relic, App Dynamics 3.Панель оперативного интеллекта - Эта линейка продуктов направлена на то, чтобы упростить оперативную команду с помощью метрик и кураторского контента, который помогает легко оставаться на вершине производительности приложений.Обычно это включает в себя объединение нескольких источников информации (журналы приложений, журналы БД, журналы серверов и т. Д.) И предварительную работу по разработке панели мониторинга.Ниже приведены несколько выбранных претендентов: Datadog, Splunk "
Выше приведен сокращенный вариант - см. Здесь больше лучших практик и примеров