Ниже приводится краткое изложение различных источников по этой теме, включая пример кода и цитаты из отдельных сообщений в блоге. Полный список лучших практик можно найти здесь
Лучшие практики обработки ошибок Node.JS
Number1: Используйте обещания для асинхронной обработки ошибок
TL; DR: обработка асинхронных ошибок в стиле обратного вызова, вероятно, является самым быстрым путем в ад (пирамида гибели). Лучший подарок, который вы можете дать своему коду, - это использовать надежную библиотеку обещаний, которая предоставляет очень компактный и знакомый синтаксис кода, такой как 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 logging»)
… Мы говорили о том, как обрабатывать ошибки, но когда вы пишете новую функцию, как вы доставляете ошибки в код, вызвавший вашу функцию? … Если вы не знаете, какие ошибки могут произойти, или не знаете, что они означают, то ваша программа может быть исправлена только случайно. Поэтому, если вы пишете новую функцию, вы должны сообщить своим вызывающим, какие ошибки могут произойти и что они имеют в виду.
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 logger в действии
//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-запросов. Можно настроить за несколько минут. Ниже приведены несколько выбранных участников: Pingdom, Uptime Robot и New Relic
2 Инструментарий кода - семейство продуктов, которым требуется встроить агента в приложение, чтобы воспользоваться функцией медленного обнаружения кода, статистики исключений, мониторинга производительности и многого другого. Ниже приведены несколько выбранных претендентов: New Relic, App Dynamics 3. Панель
оперативного интеллекта -Эти линейки продуктов направлены на содействие команде ops с помощью метрик и курируемого контента, который помогает легко оставаться на вершине производительности приложений. Обычно это включает в себя объединение нескольких источников информации (журналы приложений, журналы БД, журналы серверов и т. Д.) И предварительную работу по разработке панели мониторинга. Ниже приведены несколько избранных претендентов: Datadog, Splunk "
Выше приведен сокращенный вариант - смотрите здесь больше лучших практик и примеров