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