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может использоваться для перезапуска процесса узла, когда он падает. Лучше всего надеяться на плавное завершение работы (например, сохранить все данные в памяти в обработчике необработанных исключений).


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», который, на мой взгляд, нарушает условия сделки.


Надлежащая спецификация обещаний в 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 или навсегда, ваше приложение должно быть способно аварийно завершить работу, выйти из системы и перезагрузиться без каких-либо серьезных проблем.


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.