Я только что начал опробовать node.js несколько дней назад. Я понял, что Node завершается всякий раз, когда у меня есть необработанное исключение в моей программе. Это отличается от обычного контейнера сервера, на котором я был обнаружен, где только рабочий поток умирает, когда происходят необработанные исключения, и контейнер все равно сможет получить запрос. Это вызывает несколько вопросов:
Я был бы признателен за любой указатель/статью, которая показала бы мне общие рекомендации по обработке исключенных исключений в node.js
Ответ 2
Ниже приводится обобщение и сведение из разных источников по этой теме, включая пример кода и цитаты из выбранных сообщений в блоге. Полный список лучших практик можно найти здесь
Рекомендации по работе с ошибками Node.JS
Number1: используйте promises для обработки ошибок async
TL; DR: Обработка асинхронных ошибок в стиле обратного вызова, вероятно, является самым быстрым способом в ад (a.k.a - пирамида гибели). Лучший подарок, который вы можете дать вашему коду, использует вместо этого уважаемую библиотеку обещаний, которая обеспечивает очень компактный и знакомый синтаксис кода, такой как try-catch
В противном случае: Node.JS стиль обратного вызова, функция (err, response), является многообещающим способом для не поддерживаемого кода из-за сочетания обработки ошибок со случайным кодом, чрезмерной вложенности и неудобные шаблоны кодирования
Пример кода - хороший
doWork()
.then(doWork)
.then(doError)
.then(doWork)
.catch(errorHandler)
.then(verify);
пример кода anti pattern - обработка ошибок стиля обратного вызова
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){
...
});
});
});
});
});
Цитата в блоге: "У нас проблема с promises"
(Из блога pouchdb, занявшего 11 место по ключевым словам "Node Promises" )
"... И на самом деле, обратные вызовы делают что-то еще более зловещее: они лишают нас стека, что мы обычно принимаем как должное на языках программирования. Написание кода без стека во многом напоминает вождение автомобиля без тормоза педаль: вы не понимаете, насколько вам это нужно, пока вы не достигнете этого, а его нет. Вся суть promises заключается в том, чтобы вернуть нам основы языка, которые мы потеряли, когда мы пошли асинхронно: возвращение, бросок, и стек. Но вы должны знать, как правильно использовать promises, чтобы использовать их."
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 объект ошибки" )
"... передача строки вместо ошибки приводит к уменьшенной функциональной совместимости между модулями. Она нарушает контракты с API, которые могут выполнять проверку экземпляров Errorof, или которые хотят узнать больше об ошибке. объекты, а также видят очень интересные свойства в современных машинах JavaScript, кроме того, что сообщение передано конструктору.."
Number3: Различать операционные и программные ошибки
TL; DR: Операционные ошибки (например, API, полученные недопустимым вводом) относятся к известным случаям, когда воздействие на ошибку полностью понимается и может обрабатываться вдумчиво. С другой стороны, ошибка программиста (например, попытка прочитать переменную undefined) относится к неизвестным ошибкам кода, которые диктуют изящно перезапустить приложение
В противном случае: Вы можете всегда перезапускать приложение при появлении ошибки, но почему нужно отключать онлайн-пользователей ~ 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);
});
Цитата в блоге: "В противном случае вы рискуете состоянием"
(Из блога debugable, занимает 3 место по ключевым словам "Node.JS uncaught exception" )
"... По самой природе того, как работает бросок в JavaScript, почти никогда не существует способа безопасно" подобрать, где вы остановились ", без утечки ссылок или создания каких-либо других undefined хрупких. Самый безопасный способ ответить на возникшую ошибку - закрыть процесс. Разумеется, на обычном веб-сервере у вас может быть много соединений открытым, и не разумно резко закрыть их, потому что ошибка была вызвана кем-то другим. Лучшим подходом является отправка ответа об ошибке на запрос, вызвавший ошибку, позволяя другим закончить в обычное время и прекратить прослушивание новых запросов у этого рабочего"
Number4: обрабатывать ошибки централизованно, но не внутри промежуточного программного обеспечения
TL; DR: Логика обработки ошибок, такая как почта для администратора и ведения журнала, должна быть инкапсулирована в выделенный и централизованный объект, чтобы все конечные точки (например, промежуточное программное обеспечение Express, cron jobs, unit-testing) вызовите, когда приходит ошибка.
В противном случае: Не обрабатывать ошибки в одном месте приведет к дублированию кода и, вероятно, к ошибкам, которые обрабатываются неправильно
Пример кода - типичный поток ошибок
//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 обработка ошибок" )
"... Вы можете в конечном итоге обработать одну и ту же ошибку на нескольких уровнях стека. Это происходит, когда более низкие уровни не могут сделать ничего полезного, кроме распространения ошибки на их вызывающего абонента, которая распространяет ошибку на вызывающего абонента и т.д. Часто, только вызывающий абонент верхнего уровня знает, каков соответствующий ответ, нужно ли повторить операцию, сообщить об ошибке пользователю или что-то еще. Но это не значит, что вы должны пытаться сообщать обо всех ошибках на один обратный вызов верхнего уровня, потому что этот callback сам не знает, в каком контексте произошла ошибка"
Number5: ошибки API документа с использованием Swagger
TL; DR:. Позвольте своим абонентам API узнать, какие ошибки могут возникнуть в ответ, чтобы они могли обрабатывать эти данные без сбоев. Обычно это делается с помощью фреймворков документации REST API, таких как Swagger
В противном случае: Клиент API может решить сбой и перезапуск только потому, что он получил обратно ошибку, которую он не мог понять. Примечание: вызывающий ваш API может быть вам (очень типичный в среде микросервисов)
Цитата в блоге: "Вы должны сообщить своим абонентам, какие ошибки могут произойти"
(Из блога Joyent, занявшего 1 место по ключевым словам "Node.JS logging" )
... Мы говорили о том, как обращаться с ошибками, но когда вы пишете новую функцию, как вы доставляете ошибки в код, называемый вашей функцией?... Если вы не знаете, какие ошибки могут произойти или не знаете, что они означают, тогда ваша программа не может быть правильной, кроме как случайно. Поэтому, если вы пишете новую функцию, вы должны сообщить своим абонентам, какие ошибки могут произойти, и что они mea
Номер6: изящно завершите процесс, когда незнакомец придет в город
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. Сбалансированный подход между двумя
Номер7: используйте зрелый журнал, чтобы увеличить видимость ошибок
TL; ДР:Набор зрелых инструментов ведения журнала, таких как 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. Позволяет использовать несколько настраиваемых потоков назначения. Например, вы можете записывать журналы трассировки в один файл, но когда возникает ошибка, пишите в тот же файл, затем в файл ошибки и отправляйте электронное письмо одновременно...
Номер 8: обнаружение ошибок и простоев с использованием продуктов APM
TL; DR: Продукты мониторинга и производительности (a.k.a APM) проактивно оценивают вашу кодовую базу или API, чтобы они могли автоматически выявлять ошибки, сбои и медленные части, которые вы отсутствовали.
В противном случае:. Вы можете приложить большие усилия для измерения производительности и времени работы API, возможно, вы никогда не будете знать, какие ваши самые медленные части кода в реальном мире и как они влияют на UX
Цитата в блоге: "сегменты продуктов APM"
(Из блога Йони Голдберг)
"... Продукты APM представляют собой 3 основных сегмента: 1. Мониторинг веб-сайтов или API -, которые постоянно отслеживают время безотказной работы и производительность через HTTP-запросы. Могут быть установлены через несколько минут.: Pingdom, Uptime Robot и новая реликвия 2. Code Instrument -, для чего требуется встроить агент в приложение, чтобы использовать функцию медленного обнаружения кода, статистику исключений, мониторинг производительности и многое другое. Ниже перечислены несколько выбранных соперников: новая реликвия, динамика приложений 3. Панель управления оперативной информацией -. Эта линейка продуктов ориентирована на облегчение работы команды ops с метрикой и кураторским контентом, что помогает легко оставаться на вершине производительности приложения. Обычно это связано с объединением нескольких источников информации (журналов приложений, журналов БД, журналов серверов и т.д.) И работы по проектированию передней панели. Ниже приведены несколько выбранных соперников: Datadog, Splunk"
Выше приведенная версия - см. здесь более передовые методы и примеры
Ответ 4
nodejs domains - это самый современный способ обработки ошибок в nodejs. Домены могут захватывать как ошибки/другие события, так и традиционно заброшенные объекты. Домены также предоставляют функциональные возможности для обработки обратных вызовов с ошибкой, переданной в качестве первого аргумента методом перехвата.
Как и при обычной обработке ошибок try/catch-style, обычно лучше всего бросать ошибки, когда они происходят, и блокировать области, где вы хотите изолировать ошибки от влияния на остальную часть кода. Способ "блокировать" эти области - вызвать domain.run с помощью функции как блока изолированного кода.
В синхронном коде вышеописанное достаточно - при возникновении ошибки вы либо разрешаете ее перебрасывать, либо вы ее поймаете и обрабатываете там, возвращая любые данные, которые вам нужно вернуть.
try {
//something
} catch(e) {
// handle data reversion
// probably log too
}
Когда ошибка возникает при асинхронном обратном вызове, вам также необходимо иметь возможность полностью обрабатывать откаты данных (общее состояние, внешние данные, например базы данных и т.д.). ИЛИ вы должны установить что-то, указывающее на то, что произошло исключение - где бы вы ни заботились об этом флаге, вам нужно дождаться завершения обратного вызова.
var err = null;
var d = require('domain').create();
d.on('error', function(e) {
err = e;
// any additional error handling
}
d.run(function() { Fiber(function() {
// do stuff
var future = somethingAsynchronous();
// more stuff
future.wait(); // here we care about the error
if(err != null) {
// handle data reversion
// probably log too
}
})});
Некоторые из этого выше кода уродливы, но вы можете создавать шаблоны для себя, чтобы сделать их красивее, например:
var specialDomain = specialDomain(function() {
// do stuff
var future = somethingAsynchronous();
// more stuff
future.wait(); // here we care about the error
if(specialDomain.error()) {
// handle data reversion
// probably log too
}
}, function() { // "catch"
// any additional error handling
});
ОБНОВЛЕНИЕ (2013-09):
Выше, я использую будущее, которое подразумевает семантика волокон, которая позволяет вам ждать фьючерсов в строке. Это фактически позволяет вам использовать традиционные блоки try-catch для всего, что я считаю лучшим способом. Однако вы не всегда можете это сделать (например, в браузере)...
Есть также фьючерсы, которые не требуют семантики волокон (которые затем работают с обычным JavaScript-браузером). Их можно назвать фьючерсами, promises или отложенными (я буду просто ссылаться на фьючерсы здесь). Библиотеки фьючерсов с открытым исходным кодом JavaScript позволяют распространять ошибки между фьючерсами. Только некоторые из этих библиотек позволяют корректно обрабатывать любое запущенное будущее, поэтому будьте осторожны.
Пример:
returnsAFuture().then(function() {
console.log('1')
return doSomething() // also returns a future
}).then(function() {
console.log('2')
throw Error("oops an error was thrown")
}).then(function() {
console.log('3')
}).catch(function(exception) {
console.log('handler')
// handle the exception
}).done()
Это подражает нормальной попытке, хотя части являются асинхронными. Он будет печатать:
1
2
handler
Обратите внимание, что он не печатает '3', потому что выбрано исключение, которое прерывает этот поток.
Взгляните на синюю птицу promises:
Обратите внимание, что я не нашел много других библиотек, кроме тех, которые правильно обрабатывают заброшенные исключения. Например, jQuery отложен, например, нет - обработчик "fail" никогда не получит исключение, заставившее обработчик "then", который, на мой взгляд, является нарушителем транзакций.