Node.js Лучшая практика обработки исключений

755

Я только начал пробовать node.js несколько дней назад. Я понял, что Node завершается всякий раз, когда в моей программе возникает необработанное исключение. Это отличается от обычного серверного контейнера, с которым я столкнулся, когда только рабочий поток умирает, когда возникают необработанные исключения, и контейнер все еще сможет получать запрос. Это поднимает несколько вопросов:

  • Это process.on('uncaughtException')единственный эффективный способ защититься от этого?
  • Будет ли process.on('uncaughtException')перехватывать необработанное исключение и во время выполнения асинхронных процессов?
  • Есть ли уже созданный модуль (например, отправка электронной почты или запись в файл), который я мог бы использовать в случае неперехваченных исключений?

Я был бы признателен за любой указатель / статью, которая показала бы мне общие рекомендации по обработке необработанных исключений в node.js

момо
источник
11
необъяснимых исключений не должно быть. Если они используют программу, которая перезапускает все ваше приложение при его сбое (nodemon, навсегда, супервизор)
Raynos
116
Неисследованные исключения всегда могут произойти, если вы не поместите каждый фрагмент своего асинхронного кода внутрь try .. catchи убедитесь, что это также сделано для всех ваших библиотек
Дан
13
+1 Дан Сначала я подумал, что все ваши библиотеки были немного преувеличены, так как вам «всего лишь» нужно обернуть все свои «точки входа потока» в код в try / catches. Но если подумать об этом более тщательно, любая библиотека может иметь что- setTimeoutто setIntervalили что-то в этом роде где-то глубоко, что не может быть поймано вашим кодом.
Евгений Бересовский
8
@EugeneBeresovksy Дэн прав, но это не меняет того факта, что при возникновении исключений uncaughtExceptions единственным безопасным вариантом является перезапуск приложения. Другими словами, ваше приложение не работает, и вы ничего не можете с этим поделать или сделать. Если вы хотите сделать что-то конструктивное, внедрите новую и все еще экспериментальную функцию домена v0.8, чтобы вы могли регистрировать сбой и отправлять ответ 5xx своему клиенту.
ostergaard
1
@Dan Даже включение всех функций обратного вызова в try .. catch не гарантирует отлов ошибок. В случае, если требуемый модуль использует свои собственные двоичные файлы, они могут некорректно завершить работу. У меня такое случалось с phantomjs-node, при котором возникали ошибки, которые невозможно отловить (если я не выполнял какую-то проверку процесса на требуемых двоичных файлах, но я никогда этого не преследовал).
Trindaz

Ответы:

739

Обновление: Joyent теперь имеет свой собственный гид . Следующая информация более краткая:

Безопасно «выбрасывать» ошибки

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

  • Для синхронного кода, если произошла ошибка, верните ошибку:

    // Define divider as a syncrhonous function
    var divideSync = function(x,y) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by returning it
            return new Error("Can't divide by zero")
        }
        else {
            // no error occured, continue on
            return x/y
        }
    }
    
    // Divide 4/2
    var result = divideSync(4,2)
    // did an error occur?
    if ( result instanceof Error ) {
        // handle the error safely
        console.log('4/2=err', result)
    }
    else {
        // no error occured, continue on
        console.log('4/2='+result)
    }
    
    // Divide 4/0
    result = divideSync(4,0)
    // did an error occur?
    if ( result instanceof Error ) {
        // handle the error safely
        console.log('4/0=err', result)
    }
    else {
        // no error occured, continue on
        console.log('4/0='+result)
    }
  • Для основанного на обратном вызове (т. Е. Асинхронного) кода первый аргумент обратного вызова: errесли ошибка происходит, errэто ошибка, если ошибка не возникает, то errесть null. Любые другие аргументы следуют за errаргументом:

    var divide = function(x,y,next) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by calling the completion callback
            // with the first argument being the error
            next(new Error("Can't divide by zero"))
        }
        else {
            // no error occured, continue on
            next(null, x/y)
        }
    }
    
    divide(4,2,function(err,result){
        // did an error occur?
        if ( err ) {
            // handle the error safely
            console.log('4/2=err', err)
        }
        else {
            // no error occured, continue on
            console.log('4/2='+result)
        }
    })
    
    divide(4,0,function(err,result){
        // did an error occur?
        if ( err ) {
            // handle the error safely
            console.log('4/0=err', err)
        }
        else {
            // no error occured, continue on
            console.log('4/0='+result)
        }
    })
  • Для событийного кода, где ошибка может произойти где угодно, вместо того, чтобы выдать ошибку, запустите errorсобытие вместо :

    // Definite our Divider Event Emitter
    var events = require('events')
    var Divider = function(){
        events.EventEmitter.call(this)
    }
    require('util').inherits(Divider, events.EventEmitter)
    
    // Add the divide function
    Divider.prototype.divide = function(x,y){
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by emitting it
            var err = new Error("Can't divide by zero")
            this.emit('error', err)
        }
        else {
            // no error occured, continue on
            this.emit('divided', x, y, x/y)
        }
    
        // Chain
        return this;
    }
    
    // Create our divider and listen for errors
    var divider = new Divider()
    divider.on('error', function(err){
        // handle the error safely
        console.log(err)
    })
    divider.on('divided', function(x,y,result){
        console.log(x+'/'+y+'='+result)
    })
    
    // Divide
    divider.divide(4,2).divide(4,0)

Безопасно «ловить» ошибки

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

  • Когда мы знаем, где происходит ошибка, мы можем заключить этот раздел в домен node.js

    var d = require('domain').create()
    d.on('error', function(err){
        // handle the error safely
        console.log(err)
    })
    
    // catch the uncaught errors in this asynchronous or synchronous code block
    d.run(function(){
        // the asynchronous or synchronous code that we want to catch thrown errors on
        var err = new Error('example')
        throw err
    })
  • Если мы знаем, где происходит ошибка, это синхронный код и по какой-либо причине не можем использовать домены (возможно, старая версия узла), мы можем использовать оператор try catch:

    // catch the uncaught errors in this synchronous code block
    // try catch statements only work on synchronous code
    try {
        // the synchronous code that we want to catch thrown errors on
        var err = new Error('example')
        throw err
    } catch (err) {
        // handle the error safely
        console.log(err)
    }

    Однако будьте осторожны, чтобы не использовать их try...catchв асинхронном коде, так как асинхронно выданная ошибка не будет обнаружена:

    try {
        setTimeout(function(){
            var err = new Error('example')
            throw err
        }, 1000)
    }
    catch (err) {
        // Example error won't be caught here... crashing our app
        // hence the need for domains
    }

    Если вы хотите работать try..catchв сочетании с асинхронным кодом, при запуске узла 7.4 или выше вы можете использовать async/awaitнативно для написания своих асинхронных функций.

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

    var divide = function(x,y,next) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by calling the completion callback
            // with the first argument being the error
            next(new Error("Can't divide by zero"))
        }
        else {
            // no error occured, continue on
            next(null, x/y)
        }
    }
    
    var continueElsewhere = function(err, result){
            throw new Error('elsewhere has failed')
    }
    
    try {
            divide(4, 2, continueElsewhere)
            // ^ the execution of divide, and the execution of 
            //   continueElsewhere will be inside the try statement
    }
    catch (err) {
            console.log(err.stack)
            // ^ will output the "unexpected" result of: elsewhere has failed
    }

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

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

    // catch the uncaught errors that weren't wrapped in a domain or try catch statement
    // do not use this in modules, but only in applications, as otherwise we could have multiple of these bound
    process.on('uncaughtException', function(err) {
        // handle the error safely
        console.log(err)
    })
    
    // the asynchronous or synchronous code that emits the otherwise uncaught error
    var err = new Error('example')
    throw err
балуптон
источник
5
Спасибо Raynos, обновлено. У вас есть источник, который объясняет зло try catch? Как я хотел бы подтвердить это доказательствами. Также исправлен пример синхронизации.
Балуптон
2
Этот ответ больше не действителен. Домены решают эту проблему (рекомендуется node.js)
Габриэль Ламас
5
@balupton Ошибки должны быть выброшены для обработки ошибок. Их определенно НЕ следует избегать. В них нет ничего, что могло бы нарушить работу приложения или чего-либо еще. Java и большинство других современных языков имеют отличную поддержку исключений. Мой единственный вывод после прочтения некоторых постов, приведенных здесь, состоит в том, что люди не очень хорошо их понимают и поэтому их боятся. Страх Неопределенность Сомнение. Эта дискуссия была окончательно решена в пользу исключений по крайней мере 20 лет назад.
ent8enmentnow
22
Теперь домены объявлены устаревшими с помощью io.js : « Этот модуль ожидает устаревания. Как только API замены будет завершен, этот модуль полностью устареет… Пользователи, которые обязательно должны иметь функциональность, предоставляемую доменами, могут полагаться на него в настоящее время, но следует ожидать перехода на другое решение в будущем ».
Тимоти Гу
5
Апи домен является устаревшим в настоящее время ? Они упоминают API замены - кто-нибудь знает, когда это выйдет, и как оно будет выглядеть?
UpTheCreek
95

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


Лучшие практики обработки ошибок 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 "


Выше приведен сокращенный вариант - смотрите здесь больше лучших практик и примеров

Йонатан
источник
30

Вы можете поймать неисследованные исключения, но это ограниченное использование. См. Http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions:4c933d54-1428-443c-928d-4e1ecbdd56cb

monit, foreverили upstartможет использоваться для перезапуска процесса узла, когда он падает. Лучше всего надеяться на плавное завершение работы (например, сохранить все данные в памяти в обработчике необработанных исключений).

nponeccop
источник
4
+1 Ссылка полезная, спасибо. Я все еще ищу лучшую практику и значение «изящного перезапуска» в контексте node.js
momo
Мое понимание «постепенного перезапуска» в этом контексте было бы по сути тем, что предлагает nponeccop: пусть процесс умирает, и пусть все, что его запускает, перезапустит его.
Илкка
Большое спасибо за эту ссылку! Действительно полезно!
SatheeshJM
Это отличный ответ. Однако я не согласен с возвратом ошибки в вашем первом примере. Возвращение Errorделает возвращаемое значение полиморфным, что излишне запутывает семантику функции. Кроме того, плавание на 0 уже обработано в JavaScript, давая Infinity, -Infinityили NaN, где это значение typeof === 'number'. Их можно проверить с помощью !isFinite(value). В общем, я бы рекомендовал никогда не возвращать ошибку из функции. Лучше с точки зрения разборчивости кода и обслуживания, чтобы бросить или вернуть специальное неполиморфное значение с согласованной семантикой.
апреля
Ссылка не работает. downforeveryoneorjustme.com/debuggable.com
Кев,
13

Домены nodejs - самый современный способ обработки ошибок в nodejs. Домены могут захватывать как ошибки / другие события, так и традиционно выбрасываемые объекты. Домены также предоставляют функциональность для обработки обратных вызовов с ошибкой, переданной в качестве первого аргумента через метод перехвата.

Как и при обычной обработке ошибок в стиле try / catch, обычно лучше выбрасывать ошибки, когда они возникают, и блокировать области, где вы хотите изолировать ошибки от воздействия на остальную часть кода. Способ «заблокировать» эти области - вызвать 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-браузером). Их можно назвать фьючерсами, обещаниями или отсрочками (далее я просто буду ссылаться на фьючерсы). Фьючерсные библиотеки простого старого 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», потому что было сгенерировано исключение, которое прерывает этот поток.

Взгляните на обещания синей птицы:

Обратите внимание, что я не нашел много других библиотек, кроме этих, которые правильно обрабатывают выданные исключения. Например, jQuery deferred - обработчик «fail» никогда не получит исключение, генерирующее обработчик «then», который, на мой взгляд, нарушает условия сделки.

BT
источник
Надлежащая спецификация обещаний в Javascript известна как Promises / A +. Вы можете увидеть список реализаций здесь: github.com/promises-aplus/promises-spec/blob/master/… . Обратите внимание, что голые Promises / A + на практике непригодны - Promises / A + все еще оставляет много практических проблем для библиотек, которые они решают сами. Однако абсолютно важные вещи, такие как распространение ошибок, которое вы показываете, детерминированный порядок выполнения и безопасность от переполнения стека, гарантированы.
Esailija
11

Я писал об этом недавно на http://snmaynard.com/2012/12/21/node-error-handling/ . Новая функция узла в версии 0.8 - это домены, которые позволяют объединить все формы обработки ошибок в одну более простую форму управления. Вы можете прочитать о них в моем посте.

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

Саймон Мейнард
источник
2
Доменный модуль официально объявлен устаревшим. nodejs.org/api/domain.html
MattSidor
3

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

Ниже приводится цитата со страницы GitHub:

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

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

Михаил Ягудаев
источник
3

Один пример, где использование try-catch может быть уместным, - это использование цикла forEach. Это синхронно, но в то же время вы не можете просто использовать оператор return во внутренней области видимости. Вместо этого можно использовать подход try and catch для возврата объекта Error в соответствующей области видимости. Рассматривать:

function processArray() {
    try { 
       [1, 2, 3].forEach(function() { throw new Error('exception'); }); 
    } catch (e) { 
       return e; 
    }
}

Это комбинация подходов, описанных @balupton выше.

Михаил Ягудаев
источник
Вместо того, чтобы выдавать ошибки, некоторые разработчики рекомендуют использовать концепцию Result из Rust для возврата либо OK, либо Fail , когда сбой является известной возможностью. Это сохраняет сбои отдельно от непредвиденных ошибок. Одна реализация JS этого - r-результат .
Joeytwiddle
Это дизайнерское решение для всего приложения. Я думаю, что ваша концепция возврата ошибок примерно эквивалентна и проста для начала (без дополнительных зависимостей), но менее явна ( Result делает вас болезненно осведомленными, когда может потребоваться обработка сбоев) и менее эффективна в тех случаях, когда стек построено без необходимости.
Joeytwiddle
1

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

http://www.lighthouselogic.com/#/using-a-new-domain-for-each-async-function-in-node/

пенистый
источник
1

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

Bunyan - это популярный каркас журналирования для NodeJS - он поддерживает запись в кучу разных выходных мест, что делает его полезным для локальной отладки, если вы избегаете console.log. В обработчике ошибок вашего домена вы можете выложить ошибку в файл журнала.

var log = bunyan.createLogger({
  name: 'myapp',
  streams: [
    {
      level: 'error',
      path: '/var/tmp/myapp-error.log'  // log ERROR to this file
    }
  ]
});

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

var raygunClient = new raygun.Client().init({ apiKey: 'your API key' });
raygunClient.send(theError);

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

К. Крейвен
источник