Node.js Best Practice-Ausnahmebehandlung


755

Ich habe gerade vor ein paar Tagen angefangen, node.js auszuprobieren. Ich habe festgestellt, dass der Knoten immer dann beendet wird, wenn ich eine nicht behandelte Ausnahme in meinem Programm habe. Dies unterscheidet sich von dem normalen Servercontainer, dem ich ausgesetzt war, bei dem nur der Worker-Thread stirbt, wenn nicht behandelte Ausnahmen auftreten und der Container die Anforderung weiterhin empfangen kann. Dies wirft einige Fragen auf:

  • Ist process.on('uncaughtException')der einzig wirksame Weg, sich dagegen zu schützen?
  • Wird process.on('uncaughtException')die nicht behandelte Ausnahme auch während der Ausführung asynchroner Prozesse abgefangen?
  • Gibt es ein Modul, das bereits erstellt wurde (z. B. das Senden von E-Mails oder das Schreiben in eine Datei), das ich bei nicht erfassten Ausnahmen nutzen kann?

Ich würde mich über jeden Zeiger / Artikel freuen, der mir die gängigen Best Practices für den Umgang mit nicht erfassten Ausnahmen in node.js zeigt


11
Nicht erfasste Ausnahmen sollten nicht auftreten. Wenn sie ein Programm verwenden, das Ihre gesamte Anwendung beim Absturz neu startet (Nodemon, für immer, Supervisor)
Raynos

116
Nicht erfasste Ausnahmen können immer auftreten, wenn Sie nicht jeden Teil Ihres asynchronen Codes einfügen try .. catchund überprüfen, ob dies auch für alle Ihre Bibliotheken gilt
Dan

13
+1 Dan Zuerst dachte ich, dass alle Ihre Bibliotheken etwas übertrieben sind, da Sie "nur" alle Ihre "Thread-Einstiegspunkte" in den Code in try / catches einschließen müssen. Aber wenn Sie genauer darüber nachdenken, könnte jede Bibliothek ein setTimeoutoder setIntervaletwas Ähnliches irgendwo tief vergraben haben, das von Ihrem Code nicht erfasst werden kann.
Eugene Beresovsky

8
@EugeneBeresovksy Dan hat Recht, aber es ändert nichts an der Tatsache, dass die einzige sichere Option darin besteht, die App neu zu starten, wenn ungefangene Ausnahmen auftreten. Mit anderen Worten, Ihre App ist abgestürzt und Sie können oder sollten nichts dagegen tun. Wenn Sie etwas Konstruktives tun möchten, implementieren Sie die neue und noch experimentelle Domänenfunktion v0.8, damit Sie den Absturz protokollieren und eine 5xx-Antwort an Ihren Client senden können.
Ostergaard

1
@Dan Selbst das Einschließen aller Rückruffunktionen in try .. catch garantiert keine Fangfehler. Falls ein erforderliches Modul seine eigenen Binärdateien verwendet, können sie unanständig abstürzen. Ich habe dies mit phantomjs-node erlebt und bin bei Fehlern fehlgeschlagen, die nicht abgefangen werden können (es sei denn, ich würde eine Art Prozessinspektion der erforderlichen Binärdateien durchführen, aber das habe ich nie verfolgt).
Trindaz

Antworten:


737

Update: Joyent hat jetzt einen eigenen Guide . Die folgenden Informationen sind eher eine Zusammenfassung:

Fehler sicher "werfen"

Im Idealfall möchten wir nicht erfasste Fehler so weit wie möglich vermeiden. Anstatt den Fehler buchstäblich auszulösen, können wir den Fehler abhängig von unserer Codearchitektur mit einer der folgenden Methoden sicher "auslösen":

  • Wenn bei synchronem Code ein Fehler auftritt, geben Sie den Fehler zurück:

    // 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)
    }
  • Callback-basierte (dh. Asynchron) Code, das erste Argument des Rückrufs ist err, wenn ein Fehler passiert , errder Fehler ist, wenn ein Fehler dann nicht geschehen errist null. Alle anderen Argumente folgen dem errArgument:

    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)
        }
    })
  • Bei ereignisreichem Code, bei dem der Fehler überall auftreten kann, lösen Sie stattdessen das errorEreignis aus, anstatt den Fehler auszulösen :

    // 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)

Fehler sicher "abfangen"

Manchmal gibt es jedoch immer noch Code, der irgendwo einen Fehler auslöst, der zu einer nicht erfassten Ausnahme und einem möglichen Absturz unserer Anwendung führen kann, wenn wir ihn nicht sicher abfangen. Abhängig von unserer Codearchitektur können wir eine der folgenden Methoden verwenden, um sie abzufangen:

  • Wenn wir wissen, wo der Fehler auftritt, können wir diesen Abschnitt in eine node.js-Domäne einschließen

    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
    })
  • Wenn wir wissen, wo der Fehler auftritt, ist synchroner Code und aus irgendeinem Grund keine Domänen verwenden können (möglicherweise alte Version des Knotens), können wir die try catch-Anweisung verwenden:

    // 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)
    }

    Achten Sie jedoch darauf, dass Sie keinen try...catchasynchronen Code verwenden, da ein asynchron ausgelöster Fehler nicht abgefangen wird:

    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
    }

    Wenn Sie try..catchin Verbindung mit asynchronem Code arbeiten möchten , können Sie beim Ausführen von Knoten 7.4 oder höher async/awaitIhre asynchronen Funktionen nativ verwenden.

    Eine andere Sache, bei der Sie vorsichtig sein sollten, try...catchist das Risiko, dass Sie Ihren Abschlussrückruf trywie folgt in die Anweisung einschließen:

    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
    }

    Diese Vorgehensweise ist sehr einfach, da Ihr Code komplexer wird. Aus diesem Grund ist es am besten, entweder Domänen zu verwenden oder Fehler zurückzugeben, um (1) nicht erfasste Ausnahmen im asynchronen Code zu vermeiden. In Sprachen, die ein korrektes Threading anstelle des asynchronen Ereignismaschinenstils von JavaScript ermöglichen, ist dies weniger problematisch.

  • In dem Fall, dass ein nicht erfasster Fehler an einer Stelle auftritt, die nicht in eine Domäne eingeschlossen war, oder eine try catch-Anweisung, können wir unsere Anwendung mithilfe des uncaughtExceptionListeners nicht zum Absturz bringen (dies kann jedoch dazu führen, dass die Anwendung in einen unbekannten Zustand versetzt wird ):

    // 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
Danke Raynos, aktualisiert. Haben Sie eine Quelle, die die Übel erklärt try catch? Da würde ich das gerne mit Beweisen belegen. Das Synchronisierungsbeispiel wurde ebenfalls behoben.
Balupton

2
Diese Antwort ist nicht mehr gültig. Domains löst dieses Problem (empfohlen von node.js)
Gabriel Llamas

5
@balupton Fehler sollten zur Fehlerbehandlung ausgegeben werden. Sie sollten auf keinen Fall vermieden werden. Es gibt nichts an ihnen, was die Ausführung der App oder irgendetwas anderes unterbricht. Java und die meisten anderen modernen Sprachen unterstützen Ausnahmen hervorragend. Meine einzige Schlussfolgerung nach dem Lesen einiger der falsch formulierten Beiträge hier ist, dass die Leute sie nicht sehr gut verstehen und deshalb Angst vor ihnen haben. Fürchte unsicheren Zweifel. Diese Debatte wurde vor mindestens 20 Jahren endgültig zugunsten von Ausnahmen entschieden.
enl8enmentnow

22
Jetzt werden Domains von io.js veraltet : " Dieses Modul steht noch aus. Sobald eine Ersatz-API fertiggestellt wurde, ist dieses Modul vollständig veraltet. Benutzer, die unbedingt über die von Domains bereitgestellten Funktionen verfügen müssen, können sich vorerst darauf verlassen sollte damit rechnen, in Zukunft auf eine andere Lösung migrieren zu müssen. "
Timothy Gu

5
Die Domain-API ist jetzt veraltet ? Sie erwähnen eine Ersatz-API - weiß jemand, wann diese herauskommt und wie sie aussehen wird?
UpTheCreek

95

Es folgt eine Zusammenfassung und Kuration aus vielen verschiedenen Quellen zu diesem Thema, einschließlich Codebeispiel und Zitaten aus ausgewählten Blog-Posts. Die vollständige Liste der Best Practices finden Sie hier


Best Practices für die Fehlerbehandlung von Node.JS


Nummer 1: Verwenden Sie Versprechen für die asynchrone Fehlerbehandlung

TL; DR: Die Behandlung von asynchronen Fehlern im Rückrufstil ist wahrscheinlich der schnellste Weg zur Hölle (auch bekannt als die Pyramide des Untergangs). Das beste Geschenk, das Sie Ihrem Code machen können, ist die Verwendung einer seriösen Versprechensbibliothek, die eine sehr kompakte und vertraute Codesyntax wie Try-Catch bietet

Andernfalls: Der Node.JS-Rückrufstil, die Funktion (err, response) sind aufgrund der Mischung aus Fehlerbehandlung mit gelegentlichem Code, übermäßiger Verschachtelung und umständlichen Codierungsmustern ein vielversprechender Weg zu nicht wartbarem Code

Codebeispiel - gut

doWork()
.then(doWork)
.then(doError)
.then(doWork)
.catch(errorHandler)
.then(verify);

Codebeispiel Anti-Pattern - Fehlerbehandlung im Rückrufstil

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){ 
                    ...
                });
            });
        });
    });
});

Blog-Zitat: "Wir haben ein Problem mit Versprechungen" (Aus dem Blog pouchdb, Rang 11 für die Schlüsselwörter "Node Promises")

"... Und in der Tat tun Rückrufe etwas noch Unheimlicheres: Sie berauben uns des Stapels, was in Programmiersprachen normalerweise selbstverständlich ist. Das Schreiben von Code ohne Stapel ähnelt dem Fahren eines Autos ohne Bremspedal: Sie Weiß nicht, wie dringend du es brauchst, bis du danach greifst und es nicht da ist. Der Sinn der Versprechen besteht darin, uns die Sprachgrundlagen zurückzugeben, die wir verloren haben, als wir asynchron wurden: Zurück, werfen und den Stapel. Aber du müssen wissen, wie man Versprechen richtig einsetzt, um sie auszunutzen. "


Nummer 2: Verwenden Sie nur das integrierte Fehlerobjekt

TL; DR: Es ist ziemlich üblich, Code zu sehen, der Fehler als Zeichenfolge oder als benutzerdefinierten Typ auslöst. Dies verkompliziert die Fehlerbehandlungslogik und die Interoperabilität zwischen Modulen. Unabhängig davon, ob Sie ein Versprechen ablehnen, eine Ausnahme auslösen oder einen Fehler ausgeben - die Verwendung des integrierten Fehlerobjekts Node.JS erhöht die Einheitlichkeit und verhindert den Verlust von Fehlerinformationen

Andernfalls: Wenn Sie ein Modul ausführen und sich nicht sicher sind, welche Art von Fehlern zurückkommt, ist es viel schwieriger, über die bevorstehende Ausnahme nachzudenken und sie zu behandeln. Die Verwendung von benutzerdefinierten Typen zur Beschreibung von Fehlern kann sogar zum Verlust kritischer Fehlerinformationen wie der Stapelverfolgung führen!

Codebeispiel - richtig machen

    //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?"));

Codebeispiel Anti-Muster

//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?");

Blog-Zitat: "Eine Zeichenfolge ist kein Fehler" (Aus dem Blog-Gedanken, Rang 6 für die Schlüsselwörter "Node.JS-Fehlerobjekt")

"... das Übergeben einer Zeichenfolge anstelle eines Fehlers führt zu einer verringerten Interoperabilität zwischen Modulen. Es bricht Verträge mit APIs, die möglicherweise Instanzen von Fehlerprüfungen durchführen oder mehr über den Fehler erfahren möchten . Fehlerobjekte haben, wie wir sehen werden, sehr viel interessante Eigenschaften in modernen JavaScript-Engines neben dem Halten der an den Konstruktor übergebenen Nachricht. "


Nummer 3: Unterscheiden Sie Betriebs- und Programmierfehler

TL; DR: Betriebsfehler (z. B. API hat eine ungültige Eingabe erhalten) beziehen sich auf bekannte Fälle, in denen die Auswirkungen des Fehlers vollständig verstanden werden und sorgfältig behandelt werden können. Auf der anderen Seite bezieht sich ein Programmiererfehler (z. B. der Versuch, eine undefinierte Variable zu lesen) auf unbekannte Codefehler, die einen ordnungsgemäßen Neustart der Anwendung erfordern

Andernfalls: Sie können die Anwendung jederzeit neu starten, wenn ein Fehler auftritt. Warum sollten Sie jedoch ~ 5000 Online-Benutzer aufgrund eines geringfügigen und vorhergesagten Fehlers (Betriebsfehler) im Stich lassen? Das Gegenteil ist ebenfalls nicht ideal - das Aufrechterhalten der Anwendung, wenn ein unbekanntes Problem (Programmiererfehler) aufgetreten ist, kann zu unvorhergesehenem Verhalten führen. Die Unterscheidung der beiden ermöglicht es, taktvoll zu handeln und einen ausgewogenen Ansatz anzuwenden, der auf dem gegebenen Kontext basiert

Codebeispiel - richtig machen

    //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?"));

Codebeispiel - Markieren eines Fehlers als betriebsbereit (vertrauenswürdig)

//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);
});

Blog-Zitat : "Andernfalls riskieren Sie den Status" (Aus dem Blog debugable, Rang 3 für die Schlüsselwörter "Node.JS nicht gefangene Ausnahme")

" ... Aufgrund der Art und Weise, wie Throw in JavaScript funktioniert, gibt es fast nie eine Möglichkeit, sicher" dort weiterzumachen, wo Sie aufgehört haben ", ohne Referenzen zu verlieren oder einen anderen undefinierten, spröden Zustand zu erzeugen. Der sicherste Weg, darauf zu reagieren Ein ausgelöster Fehler besteht darin, den Prozess herunterzufahren . Auf einem normalen Webserver sind möglicherweise viele Verbindungen geöffnet, und es ist nicht sinnvoll, diese abrupt herunterzufahren, da ein Fehler von einer anderen Person ausgelöst wurde Senden Sie eine Fehlerantwort auf die Anforderung, die den Fehler ausgelöst hat, während die anderen in ihrer normalen Zeit fertig sind, und hören Sie auf, auf neue Anforderungen in diesem Worker zu warten. "


Nummer 4: Behandeln Sie Fehler zentral, jedoch nicht innerhalb der Middleware

TL; DR: Fehlerbehandlungslogik wie E-Mail an den Administrator und Protokollierung sollte in einem dedizierten und zentralisierten Objekt gekapselt sein, das alle Endpunkte (z. B. Express-Middleware, Cron-Jobs, Komponententests) aufrufen, wenn ein Fehler auftritt.

Andernfalls: Wenn Fehler nicht an einem einzigen Ort behandelt werden, führt dies zu einer Duplizierung des Codes und möglicherweise zu Fehlern, die nicht ordnungsgemäß behandelt werden

Codebeispiel - ein typischer Fehlerfluss

//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);
    });
});

Blog-Zitat: "Manchmal können niedrigere Ebenen nichts Nützliches tun, als den Fehler an ihren Anrufer weiterzugeben" (Aus dem Blog Joyent, Rang 1 für die Schlüsselwörter "Node.JS-Fehlerbehandlung")

"... Möglicherweise wird derselbe Fehler auf mehreren Ebenen des Stapels behandelt. Dies geschieht, wenn niedrigere Ebenen nichts Nützliches tun können, als den Fehler an ihren Aufrufer weiterzugeben, der den Fehler an seinen Aufrufer weitergibt, und so weiter. Nur der Anrufer der obersten Ebene weiß, wie die entsprechende Antwort lautet, ob er den Vorgang wiederholen, dem Benutzer einen Fehler melden oder etwas anderes. Dies bedeutet jedoch nicht, dass Sie versuchen sollten, alle Fehler einer einzelnen obersten Ebene zu melden Rückruf, da dieser Rückruf selbst nicht wissen kann, in welchem ​​Kontext der Fehler aufgetreten ist "


Nummer 5: Dokument-API-Fehler mit Swagger

TL; DR: Lassen Sie Ihre API-Aufrufer wissen, welche Fehler möglicherweise auftreten, damit sie diese nachdenklich behandeln können, ohne abzustürzen. Dies erfolgt normalerweise mit REST-API-Dokumentationsframeworks wie Swagger

Andernfalls: Ein API-Client entscheidet sich möglicherweise nur zum Absturz und Neustart, weil er einen Fehler erhalten hat, den er nicht verstehen konnte. Hinweis: Der Aufrufer Ihrer API könnten Sie sein (sehr typisch in einer Microservices-Umgebung).

Blog-Zitat: "Sie müssen Ihren Anrufern mitteilen, welche Fehler auftreten können" (Aus dem Blog Joyent, Rang 1 für die Schlüsselwörter "Node.JS-Protokollierung")

… Wir haben darüber gesprochen, wie mit Fehlern umgegangen werden soll. Wie können Sie beim Schreiben einer neuen Funktion Fehler an den Code senden, der Ihre Funktion aufgerufen hat? … Wenn Sie nicht wissen, welche Fehler auftreten können oder was sie bedeuten, kann Ihr Programm nur aus Versehen korrekt sein. Wenn Sie also eine neue Funktion schreiben, müssen Sie Ihren Anrufern mitteilen, welche Fehler auftreten können und was sie bedeuten


Nummer 6: Schließen Sie den Prozess elegant, wenn ein Fremder in die Stadt kommt

TL; DR: Wenn ein unbekannter Fehler auftritt (ein Entwicklerfehler, siehe Best Practice Nr. 3), besteht Unsicherheit über den Zustand der Anwendung. Eine gängige Praxis schlägt vor, den Prozess sorgfältig mit einem Neustart-Tool wie Forever und PM2 neu zu starten

Andernfalls: Wenn eine unbekannte Ausnahme abgefangen wird, befindet sich möglicherweise ein Objekt in einem fehlerhaften Zustand (z. B. ein Ereignisemitter, der global verwendet wird und aufgrund eines internen Fehlers keine Ereignisse mehr auslöst), und alle zukünftigen Anforderungen können fehlschlagen oder sich verrückt verhalten

Codebeispiel - Entscheidung, ob abgestürzt werden soll

//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;
 }

Blog-Zitat: "Es gibt drei Denkschulen zur Fehlerbehandlung" (Aus dem Blog jsrecipes)

… Es gibt hauptsächlich drei Denkansätze zur Fehlerbehandlung: 1. Lassen Sie die Anwendung abstürzen und starten Sie sie neu. 2. Behandeln Sie alle möglichen Fehler und stürzen Sie niemals ab. 3. Ausgewogener Ansatz zwischen den beiden


Nummer 7: Verwenden Sie einen ausgereiften Logger, um die Sichtbarkeit von Fehlern zu erhöhen

TL; DR: Eine Reihe ausgereifter Protokollierungswerkzeuge wie Winston, Bunyan oder Log4J beschleunigen die Fehlererkennung und das Verständnis. Vergessen Sie also console.log.

Andernfalls: Durchblättern von console.logs oder manuell durch unordentliche Textdateien ohne Abfrage von Tools oder eines anständigen Protokollbetrachters können Sie bis spät in die Arbeit beschäftigt sein

Codebeispiel - Winston Logger in Aktion

//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' });

Blog-Zitat: "Lassen Sie uns einige Anforderungen identifizieren (für einen Logger):" (Aus dem Blog-Strongblog)

… Lassen Sie uns einige Anforderungen (für einen Logger) identifizieren: 1. Zeitstempel für jede Logzeile. Dieser ist ziemlich selbsterklärend - Sie sollten in der Lage sein zu erkennen, wann jeder Protokolleintrag aufgetreten ist. 2. Das Protokollierungsformat sollte sowohl für Menschen als auch für Maschinen leicht verdaulich sein. 3. Ermöglicht mehrere konfigurierbare Zielströme. Beispielsweise schreiben Sie möglicherweise Ablaufverfolgungsprotokolle in eine Datei. Wenn jedoch ein Fehler auftritt, schreiben Sie in dieselbe Datei, dann in eine Fehlerdatei und senden Sie gleichzeitig eine E-Mail.


Nummer 8: Erkennen Sie Fehler und Ausfallzeiten mithilfe von APM-Produkten

TL; DR: Überwachungs- und Leistungsprodukte (auch bekannt als APM) messen Ihre Codebasis oder API proaktiv, damit sie fehlende Fehler, Abstürze und langsame Teile automatisch auf magische Weise hervorheben können

Andernfalls: Sie werden möglicherweise große Anstrengungen unternehmen, um die API-Leistung und Ausfallzeiten zu messen. Wahrscheinlich werden Sie nie wissen, welche Codeteile im realen Szenario am langsamsten sind und wie sich diese auf die UX auswirken

Blog-Zitat: "APM-Produktsegmente" (Aus dem Blog Yoni Goldberg)

"… APM-Produkte bilden drei Hauptsegmente: 1. Website- oder API-Überwachung - externe Dienste, die die Verfügbarkeit und Leistung über HTTP-Anforderungen ständig überwachen. Sie können in wenigen Minuten eingerichtet werden. Es folgen einige ausgewählte Konkurrenten: Pingdom, Uptime Robot und New Relic 2 Code-Instrumentierung - Produktfamilie, bei der ein Agent in die Anwendung eingebettet werden muss, um eine langsame Codeerkennung, Ausnahmestatistiken, Leistungsüberwachung und vieles mehr zu ermöglichen. Im Folgenden sind einige ausgewählte Konkurrenten aufgeführt: New Relic, App Dynamics 3. Dashboard für Operational Intelligence -Diese Produktlinie konzentriert sich darauf, dem Ops-Team Messdaten und kuratierte Inhalte zur Verfügung zu stellen, mit denen Sie die Anwendungsleistung auf dem neuesten Stand halten können. Dies umfasst normalerweise das Aggregieren mehrerer Informationsquellen (Anwendungsprotokolle, DB-Protokolle, Serverprotokolle usw.) und die Entwurfsarbeit für das Dashboard im Voraus. Es folgen einige ausgewählte Kandidaten: Datadog, Splunk "


Das Obige ist eine verkürzte Version - siehe hier mehr Best Practices und Beispiele


30

Sie können nicht erfasste Ausnahmen abfangen, aber es ist von begrenztem Nutzen. Siehe http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions:4c933d54-1428-443c-928d-4e1ecbdd56cb

monit, foreveroder upstartkann verwendet werden, um den Knotenprozess neu zu starten, wenn er abstürzt. Ein ordnungsgemäßes Herunterfahren ist das Beste, auf das Sie hoffen können (z. B. Speichern aller speicherinternen Daten im nicht erfassten Ausnahmebehandler).


4
+1 Der Link ist nützlich, danke. Ich bin immer noch auf der Suche nach der besten Vorgehensweise und der Bedeutung von "ordnungsgemäßem Neustart" im Kontext von node.js
momo

Mein Verständnis von "würdevollem Neustart" in diesem Zusammenhang wäre im Wesentlichen das, was nponeccop vorschlägt: Lassen Sie den Prozess sterben und lassen Sie alles, was ihn überhaupt ausführt, neu starten.
Ilkka

Vielen Dank für diesen Link! Sehr nützlich!
SatheeshJM

Dies ist eine großartige Antwort. Ich würde jedoch nicht zustimmen, einen Fehler in Ihrem ersten Beispiel zurückzugeben. Durch Errordie Rückgabe von wird der Rückgabewert polymorph, wodurch die Semantik der Funktion unnötig durcheinander gebracht wird. Außerdem tauchen von 0 bereits in JavaScript , indem sie behandelt Infinity, -Infinityoder die NaNWerte , wo typeof === 'number'. Sie können mit überprüft werden !isFinite(value). Im Allgemeinen würde ich empfehlen, niemals einen Fehler von einer Funktion zurückzugeben. Besser in Bezug auf Code-Lesbarkeit und Wartung, um einen speziellen nicht polymorphen Wert mit konsistenter Semantik zu werfen oder zurückzugeben.
wprl

Die Verbindung ist unterbrochen. downforeveryoneorjustme.com/debuggable.com
Kev

13

nodejs-Domänen sind die aktuellste Methode zur Behandlung von Fehlern in nodejs. Domänen können sowohl Fehler- / andere Ereignisse als auch traditionell ausgelöste Objekte erfassen. Domänen bieten auch Funktionen zum Behandeln von Rückrufen mit einem Fehler, der als erstes Argument über die Intercept-Methode übergeben wird.

Wie bei der normalen Fehlerbehandlung im Try / Catch-Stil ist es normalerweise am besten, Fehler auszulösen, wenn sie auftreten, und Bereiche auszublenden, in denen Sie Fehler davon abhalten möchten, den Rest des Codes zu beeinflussen. Die Möglichkeit, diese Bereiche zu "blockieren", besteht darin, domain.run mit einer Funktion als Block isolierten Codes aufzurufen.

Im synchronen Code reicht das oben Genannte aus: Wenn ein Fehler auftritt, lassen Sie ihn entweder durchwerfen oder Sie fangen ihn ab und behandeln ihn dort, wobei Sie alle Daten zurücksetzen, die Sie zurücksetzen müssen.

try {  
  //something
} catch(e) {
  // handle data reversion
  // probably log too
}

Wenn der Fehler bei einem asynchronen Rückruf auftritt, müssen Sie entweder in der Lage sein, das Rollback von Daten (gemeinsam genutzter Status, externe Daten wie Datenbanken usw.) vollständig zu verarbeiten. ODER Sie müssen etwas festlegen, um anzuzeigen, dass eine Ausnahme aufgetreten ist. Wo immer Sie sich für dieses Flag interessieren, müssen Sie warten, bis der Rückruf abgeschlossen ist.

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
  }

})});

Ein Teil des obigen Codes ist hässlich, aber Sie können Muster für sich selbst erstellen, um ihn schöner zu machen, z.

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
});

UPDATE (2013-09):

Oben, verwende ich eine Zukunft , die impliziert Fasern Semantik , die Sie auf Futures in-line warten lassen. Auf diese Weise können Sie für alles traditionelle Try-Catch-Blöcke verwenden - was meiner Meinung nach der beste Weg ist. Sie können dies jedoch nicht immer tun (dh im Browser) ...

Es gibt auch Futures, für die keine Fasersemantik erforderlich ist (die dann mit normalem, browserartigem JavaScript funktioniert). Diese können als Futures, Versprechen oder Aufgeschobene bezeichnet werden (ich werde von nun an nur noch auf Futures verweisen). Mit einfachen JavaScript-Futures-Bibliotheken können Fehler zwischen Futures übertragen werden. Nur einige dieser Bibliotheken ermöglichen die korrekte Handhabung einer geworfenen Zukunft. Seien Sie also vorsichtig.

Ein Beispiel:

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()

Dies ahmt einen normalen Try-Catch nach, obwohl die Teile asynchron sind. Es würde drucken:

1
2
handler

Beachten Sie, dass '3' nicht ausgegeben wird, da eine Ausnahme ausgelöst wurde, die diesen Fluss unterbricht.

Schauen Sie sich die Versprechen von Bluebird an:

Beachten Sie, dass ich nicht viele andere Bibliotheken als diese gefunden habe, die ausgelöste Ausnahmen richtig behandeln. jQuery hat es zum Beispiel nicht aufgeschoben - der "Fail" -Handler würde niemals die Ausnahme als "Dann" -Handler auslösen, was meiner Meinung nach ein Deal Breaker ist.


Die richtige Versprechensspezifikation in Javascript ist als Versprechungen / A + bekannt. Eine Liste der Implementierungen finden Sie hier: github.com/promises-aplus/promises-spec/blob/master/… . Beachten Sie, dass ein bloßes Versprechen / A + in der Praxis unbrauchbar ist - Versprechen / A + lässt Bibliotheken immer noch viele praktische Probleme, um sich selbst zu lösen. Es sind jedoch absolut wesentliche Dinge wie die von Ihnen gezeigte Fehlerausbreitung, die deterministische Ausführungsreihenfolge und die Sicherheit vor Stapelüberlauf garantiert.
Esailija


11

Ich habe kürzlich darüber unter http://snmaynard.com/2012/12/21/node-error-handling/ geschrieben . Eine neue Funktion von Node in Version 0.8 sind Domänen, mit denen Sie alle Formen der Fehlerbehandlung in einem einfacheren Verwaltungsformular kombinieren können. Sie können darüber in meinem Beitrag lesen.

Sie können auch so etwas wie Bugsnag verwenden , um Ihre nicht erfassten Ausnahmen zu verfolgen und per E-Mail, Chatroom benachrichtigt zu werden oder ein Ticket für eine nicht erfasste Ausnahme erstellen zu lassen (ich bin der Mitbegründer von Bugsnag).


2
Das Domain-Modul ist jetzt offiziell veraltet. nodejs.org/api/domain.html
MattSidor

3

Ich möchte nur hinzufügen, dass die Step.js-Bibliothek Ihnen hilft, Ausnahmen zu behandeln, indem Sie sie immer an die nächste Schrittfunktion übergeben. Daher können Sie als letzten Schritt eine Funktion haben, die in den vorherigen Schritten nach Fehlern sucht. Dieser Ansatz kann Ihre Fehlerbehandlung erheblich vereinfachen.

Unten ist ein Zitat von der Github-Seite:

Alle ausgelösten Ausnahmen werden abgefangen und als erstes Argument an die nächste Funktion übergeben. Solange Sie keine Rückruffunktionen in Ihre Hauptfunktionen einbetten, wird verhindert, dass es jemals zu nicht erfassten Ausnahmen kommt. Dies ist sehr wichtig für lange laufende node.JS-Server, da eine einzelne nicht erfasste Ausnahme den gesamten Server zum Absturz bringen kann.

Darüber hinaus können Sie mit Step die Ausführung von Skripten steuern, um als letzten Schritt einen Bereinigungsabschnitt zu erhalten. Wenn Sie beispielsweise ein Build-Skript in Node schreiben und angeben möchten, wie lange das Schreiben gedauert hat, können Sie dies im letzten Schritt tun (anstatt zu versuchen, den letzten Rückruf auszuloten).


3

Ein Fall, in dem die Verwendung eines Try-Catch angemessen sein könnte, ist die Verwendung einer forEach-Schleife. Es ist synchron, aber gleichzeitig können Sie nicht einfach eine return-Anweisung im inneren Bereich verwenden. Stattdessen kann ein Try-and-Catch-Ansatz verwendet werden, um ein Fehlerobjekt im entsprechenden Bereich zurückzugeben. Erwägen:

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

Es ist eine Kombination der oben von @balupton beschriebenen Ansätze.


Statt Fehler zu werfen, einige Entwickler empfehlen das Ergebnis Konzept von Rust entweder eine Rückkehr OK oder ein Ausfallen , wenn ein Ausfall eine bekannte Möglichkeit ist. Dies hält Fehler von unerwarteten Fehlern getrennt. Eine JS-Implementierung davon ist r-result .
Joeytwiddle

Es ist eine App-weite Designentscheidung. Ich denke, Ihr Konzept, Fehler zurückzugeben, ist ungefähr gleichwertig und einfach zu beginnen (keine zusätzlichen Abhängigkeiten), aber weniger explizit ( Ergebnis macht Ihnen schmerzlich bewusst, wann Fehler behandelt werden müssen) und in solchen Fällen, in denen ein Stapel vorhanden ist, weniger effizient unnötig gebaut.
Joeytwiddle

1

Nachdem ich diesen Beitrag vor einiger Zeit gelesen hatte, fragte ich mich, ob es sicher ist, Domänen für die Ausnahmebehandlung auf API- / Funktionsebene zu verwenden. Ich wollte sie verwenden, um den Code für die Ausnahmebehandlung in jeder von mir geschriebenen asynchronen Funktion zu vereinfachen. Ich befürchtete, dass die Verwendung einer neuen Domäne für jede Funktion einen erheblichen Overhead verursachen würde. Meine Hausaufgaben scheinen darauf hinzudeuten, dass es nur minimalen Overhead gibt und dass die Leistung bei Domains in einigen Situationen tatsächlich besser ist als bei Try Catch.

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


1

Das Abfangen von Fehlern wurde hier sehr gut besprochen, aber es lohnt sich, daran zu denken, die Fehler irgendwo abzumelden, damit Sie sie anzeigen und Fehler beheben können.

Bunyan ist ein beliebtes Protokollierungsframework für NodeJS - es unterstützt das Schreiben an eine Reihe verschiedener Ausgabeorte, was es für das lokale Debuggen nützlich macht, solange Sie console.log vermeiden. In der Fehlerbehandlungsroutine Ihrer Domain können Sie den Fehler in eine Protokolldatei ausspucken.

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

Dies kann zeitaufwändig werden, wenn Sie viele Fehler und / oder Server überprüfen müssen. Es kann sich daher lohnen, ein Tool wie Raygun (Haftungsausschluss, ich arbeite bei Raygun) zu verwenden, um Fehler zu gruppieren - oder beide zusammen zu verwenden. Wenn Sie sich für Raygun als Tool entschieden haben, ist die Einrichtung ebenfalls recht einfach

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

Wenn Sie ein Tool wie PM2 oder für immer verwenden, sollte Ihre App in der Lage sein, ohne größere Probleme abzustürzen, sich abzumelden und neu zu starten.


Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.