Gestione delle eccezioni delle migliori pratiche Node.js


755

Ho appena iniziato a provare node.js qualche giorno fa. Mi sono reso conto che il nodo viene chiuso ogni volta che ho un'eccezione non gestita nel mio programma. Ciò è diverso dal normale contenitore di server a cui sono stato esposto dove muore solo il thread di lavoro quando si verificano eccezioni non gestite e il contenitore sarà comunque in grado di ricevere la richiesta. Ciò solleva alcune domande:

  • È process.on('uncaughtException')l'unico modo efficace per impedire che essa?
  • Sarà process.on('uncaughtException')catturare l'eccezione non gestita durante l'esecuzione dei processi asincroni come bene?
  • Esiste già un modulo (come l'invio di e-mail o la scrittura su un file) che potrei sfruttare in caso di eccezioni non rilevate?

Gradirei qualsiasi puntatore / articolo che mi mostrasse le migliori pratiche comuni per la gestione delle eccezioni non rilevate in node.js


11
non dovrebbero verificarsi eccezioni non rilevate. Se lo fanno utilizzare un programma che riavvia l'intera applicazione quando il suo crash (nodemon, per sempre, supervisore)
Raynos

116
Eccezioni non rilevate possono sempre accadere a meno che tu non inserisca ogni parte del tuo codice asincrono try .. catche controlli che ciò avvenga anche per tutte le tue librerie
Dan

13
+1 Dan All'inizio ho pensato che tutte le tue librerie fossero un po 'esagerate, dato che "solo" devi racchiudere tutti i tuoi "punti di accesso al thread" nel codice in try / catch. Ma pensandoci più attentamente, qualsiasi lib potrebbe avere un setTimeouto setIntervalquel tipo di quel tipo seppellito da qualche parte nel profondo che non può essere catturato dal tuo codice.
Eugene Beresovsky,

8
@EugeneBeresovksy Dan ha ragione, ma non cambia il fatto che quando si verificano eccezioni non rilevate l'unica opzione sicura è riavviare l'app. In altre parole, la tua app si è arrestata in modo anomalo e non c'è nulla che tu possa fare o che debba fare al riguardo. Se vuoi fare qualcosa di costruttivo implementare la nuova funzionalità di dominio v0.8, ancora sperimentale, in modo da poter registrare il crash e inviare una risposta 5xx al tuo client.
ostergaard

1
@Dan Anche racchiudere tutte le funzioni di callback in try .. catch non garantisce errori di cattura. Nel caso in cui un modulo richiesto utilizzi i propri binari, possono bloccarsi in modo sgraziato. Ho avuto questo accadere con phantomjs-node, fallendo su errori che sono impossibili da rilevare (a meno che non dovessi fare una sorta di ispezione del processo sui binari richiesti, ma non l'ho mai perseguito).
Trindaz,

Risposte:


738

Aggiornamento: Joyent ora ha la sua guida . Le seguenti informazioni sono più di un riepilogo:

Errori di "lancio" in sicurezza

Idealmente vorremmo evitare il più possibile errori non rilevati, in quanto tali, invece di lanciare letteralmente l'errore, possiamo invece "lanciare" in modo sicuro l'errore utilizzando uno dei seguenti metodi a seconda della nostra architettura di codice:

  • Per il codice sincrono, se si verifica un errore, restituire l'errore:

    // 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)
    }
  • Per il codice basato su callback (cioè asincrono), il primo argomento del callback è err, se si verifica un errore errè l'errore, se non si verifica un errore, lo errè null. Qualsiasi altro argomento segue l' errargomento:

    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)
        }
    })
  • Per il codice ricco di eventi , in cui l'errore può verificarsi ovunque, invece di generare l'errore, attiva errorinvece l' evento :

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

Errori di "cattura" sicuri

A volte, tuttavia, potrebbe esserci ancora del codice che genera un errore da qualche parte che può portare a un'eccezione non rilevata e a un potenziale arresto anomalo della nostra applicazione se non la rileviamo in modo sicuro. A seconda della nostra architettura di codice, possiamo utilizzare uno dei seguenti metodi per catturarlo:

  • Quando sappiamo dove si verifica l'errore, possiamo racchiudere quella sezione in un dominio 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
    })
  • Se sappiamo dove si verifica l'errore è il codice sincrono e per qualsiasi motivo non è possibile utilizzare i domini (forse la vecchia versione del nodo), possiamo usare l'istruzione 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)
    }

    Tuttavia, fare attenzione a non utilizzare try...catchnel codice asincrono, poiché non verrà rilevato un errore generato in modo asincrono:

    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
    }

    Se si desidera lavorare try..catchinsieme al codice asincrono, è possibile utilizzare il nodo 7.4 o versioni successiveasync/await modo nativo per scrivere le funzioni asincrone.

    Un'altra cosa a cui prestare attenzione try...catchè il rischio di racchiudere il callback di completamento all'interno dell'istruzione in questo trymodo:

    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
    }

    Questo gotcha è molto facile da fare poiché il codice diventa più complesso. Pertanto, è meglio utilizzare domini o restituire errori per evitare (1) eccezioni non rilevate nel codice asincrono (2) per provare a catturare l'esecuzione che non si desidera. Nei linguaggi che consentono il threading corretto anziché lo stile asincrono di eventi-macchina di JavaScript, questo è meno un problema.

  • Infine, nel caso in cui si verifichi un errore non rilevato in un luogo che non è stato racchiuso in un dominio o in un'istruzione try catch, possiamo fare in modo che la nostra applicazione non si arresti in modo anomalo utilizzando l' uncaughtExceptionascoltatore (tuttavia ciò può portare l'applicazione in uno stato sconosciuto ):

    // 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
Grazie Raynos, aggiornato. Hai una fonte che spiega i mali di try catch? Come mi piacerebbe supportarlo con prove. Risolto anche l'esempio di sincronizzazione.
Balupton,

2
Questa risposta non è più valida. Domini risolve questo problema (consigliato da node.js)
Gabriel Llamas,

5
@balupton Gli errori devono essere generati per la gestione degli errori. Non dovrebbero assolutamente essere evitati. Non c'è nulla su di loro che interrompe l'esecuzione dell'app o qualsiasi altra cosa. Java e molti altri linguaggi moderni offrono un eccellente supporto per le eccezioni. La mia unica conclusione dopo aver letto alcuni dei post male informati qui è che le persone non li capiscono molto bene e quindi hanno paura di loro. Paura dubbio incerto. Questo dibattito è stato definitivamente deciso a favore delle eccezioni almeno 20 anni fa.
enl8enmentnow

22
Ora i domini sono deprecati da io.js : " Questo modulo è in attesa di deprecazione. Una volta finalizzata un'API sostitutiva, questo modulo sarà completamente deprecato ... Gli utenti che devono assolutamente avere la funzionalità fornita dai domini possono fare affidamento su di esso per il momento ma dovrebbe aspettarsi di dover migrare verso una soluzione diversa in futuro. "
Timothy Gu,

5
L' API di dominio è obsoleto ora ? Citano un'API sostitutiva: qualcuno sa quando uscirà e come sarà?
UpTheCreek

95

Di seguito è riportato un riepilogo e una cura di molte fonti diverse su questo argomento, inclusi esempi di codice e citazioni da post di blog selezionati. L'elenco completo delle migliori pratiche è disponibile qui


Best practice per la gestione degli errori Node.JS


Numero 1: utilizzare le promesse per la gestione degli errori asincroni

TL; DR: gestire gli errori asincroni nello stile di callback è probabilmente il modo più veloce per l'inferno (ovvero la piramide del destino). Il miglior regalo che puoi fare al tuo codice è usare invece una stimabile libreria promessa che fornisce una sintassi molto compatta e familiare come try-catch

Altrimenti: lo stile di callback Node.JS, la funzione (err, response), è un modo promettente di codice non gestibile a causa del mix di gestione degli errori con codice casuale, nidificazione eccessiva e schemi di codifica scomodi

Esempio di codice: buono

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

esempio di codice anti-pattern - gestione degli errori in stile callback

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

Citazione del blog: "Abbiamo un problema con le promesse" (Dal blog pouchdb, classificato 11 per le parole chiave "Node Promises")

"... E in effetti, i callback fanno qualcosa di ancora più sinistro: ci privano dello stack, che è qualcosa che di solito diamo per scontato nei linguaggi di programmazione. Scrivere codice senza uno stack è un po 'come guidare un'auto senza un pedale del freno: tu non ti rendi conto di quanto ne hai bisogno, fino a quando non lo raggiungi e non è lì. Il punto centrale delle promesse è di restituirci i fondamenti linguistici che abbiamo perso quando siamo andati asincroni: ritorno, lancio e stack. saper usare correttamente le promesse per poterle sfruttare " .


Numero 2: utilizzare solo l'oggetto Error incorporato

TL; DR: abbastanza comune vedere il codice che genera errori come stringa o come tipo personalizzato: ciò complica la logica di gestione degli errori e l'interoperabilità tra i moduli. Indipendentemente dal fatto che rifiuti una promessa, generi un'eccezione o emetta un errore, l'utilizzo dell'oggetto Errore incorporato Node.JS aumenta l'uniformità e previene la perdita di informazioni sull'errore

Altrimenti: quando si esegue un modulo, essendo incerti sul tipo di errori che vengono restituiti, è molto più difficile ragionare sulla prossima eccezione e gestirla. Vale la pena, l'uso di tipi personalizzati per descrivere gli errori potrebbe portare alla perdita di informazioni critiche sull'errore come la traccia dello stack!

Esempio di codice: farlo nel modo giusto

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

esempio di codice anti pattern

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

Citazione del blog: "Una stringa non è un errore" (Dal blog devthought, classificato 6 per le parole chiave "oggetto errore Node.JS")

"... il passaggio di una stringa anziché un errore comporta una ridotta interoperabilità tra i moduli. Rompono i contratti con le API che potrebbero eseguire istanze di controllo degli errori o che desiderano saperne di più sull'errore . Gli oggetti di errore, come vedremo, hanno molto proprietà interessanti nei moderni motori JavaScript oltre a contenere il messaggio passato al costruttore .. "


Numero 3: distinguere gli errori operativi rispetto al programmatore

TL; DR: gli errori operativi (ad es. L'API hanno ricevuto un input non valido) si riferiscono a casi noti in cui l'impatto dell'errore è completamente compreso e può essere gestito in modo ponderato. D'altra parte, l'errore del programmatore (ad es. Tentativo di leggere una variabile non definita) si riferisce a errori di codice sconosciuti che impongono di riavviare correttamente l'applicazione

Altrimenti: è sempre possibile riavviare l'applicazione quando viene visualizzato un errore, ma perché deludere ~ 5000 utenti online a causa di un errore minore e previsto (errore operativo)? anche il contrario non è l'ideale: mantenere l'applicazione in alto quando si verifica un problema sconosciuto (errore del programmatore) potrebbe comportare un comportamento non previsto. Differenziare i due consente di agire con tatto e applicare un approccio equilibrato basato sul contesto dato

Esempio di codice: farlo nel modo giusto

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

esempio di codice: contrassegnare un errore come operativo (attendibile)

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

Citazione del blog : "Altrimenti rischi lo stato" (Dal blog debugable, classificato 3 per le parole chiave "Node.JS non rilevata eccezione")

" ... Per la stessa natura di come funziona il lancio in JavaScript, non c'è quasi mai alcun modo di" riprendere da dove eri rimasto ", senza perdere riferimenti, o creare qualche altro tipo di stato fragile indefinito. Il modo più sicuro per rispondere a un errore generato è quello di chiudere il processo . Naturalmente, in un normale server Web, potresti avere molte connessioni aperte e non è ragionevole chiuderle bruscamente perché un errore è stato attivato da qualcun altro. L'approccio migliore è quello di invia una risposta di errore alla richiesta che ha generato l'errore, lasciando che gli altri finiscano il loro tempo normale e smetta di ascoltare nuove richieste in quel lavoratore "


Numero 4: gestire gli errori centralmente, attraverso ma non all'interno del middleware

TL; DR: errore nella gestione della logica come la posta all'amministratore e la registrazione devono essere incapsulati in un oggetto dedicato e centralizzato che tutti gli endpoint (ad es. Middleware Express, cron job, unit test) chiamano quando si verifica un errore.

Altrimenti: la mancata gestione degli errori all'interno di una singola posizione comporta la duplicazione del codice e probabilmente errori gestiti in modo improprio

Esempio di codice: un tipico flusso di errori

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

Citazione del blog: "A volte i livelli inferiori non possono fare nulla di utile se non propagare l'errore al chiamante" (Dal blog Joyent, classificato 1 per le parole chiave "Gestione degli errori Node.JS")

"... Potresti finire per gestire lo stesso errore a più livelli dello stack. Ciò accade quando i livelli inferiori non possono fare nulla di utile se non propagare l'errore al loro chiamante, che propaga l'errore al suo chiamante e così via. Spesso, solo il chiamante di livello superiore sa quale sia la risposta appropriata, se si tratta di riprovare l'operazione, segnalare un errore all'utente o qualcos'altro, ma ciò non significa che si dovrebbe provare a segnalare tutti gli errori a un singolo livello superiore callback, perché il callback stesso non può sapere in quale contesto si è verificato l'errore "


Numero 5: Documentare gli errori dell'API usando Swagger

TL; DR: fai sapere ai chiamanti delle API quali errori potrebbero verificarsi in cambio in modo che possano gestirli in modo ponderato senza arresti anomali. Questo di solito viene fatto con framework di documentazione API REST come Swagger

Altrimenti: un client API potrebbe decidere di arrestarsi in modo anomalo e riavviarsi solo perché ha ricevuto un errore che non riusciva a capire. Nota: il chiamante della tua API potrebbe essere tu (molto tipico in un ambiente di microservizi)

Citazione del blog: "Devi dire ai tuoi chiamanti quali errori possono verificarsi" (Dal blog Joyent, classificato 1 per le parole chiave "registrazione Node.JS")

... Abbiamo parlato di come gestire gli errori, ma quando si scrive una nuova funzione, come si distribuiscono gli errori al codice che ha chiamato la funzione? ... Se non sai quali errori possono verificarsi o non sai cosa significano, il tuo programma non può essere corretto se non per caso. Quindi, se stai scrivendo una nuova funzione, devi dire ai tuoi chiamanti quali errori possono accadere e cosa significano


Numero 6: chiudi il processo con grazia quando uno sconosciuto arriva in città

TL; DR: quando si verifica un errore sconosciuto (un errore dello sviluppatore, vedere il numero di best practice n. 3) - c'è incertezza sulla salubrità dell'applicazione. Una pratica comune suggerisce di riavviare il processo con attenzione utilizzando uno strumento di "riavvio" come Forever e PM2

Altrimenti: quando viene rilevata un'eccezione sconosciuta, alcuni oggetti potrebbero trovarsi in uno stato difettoso (ad es. Un emettitore di eventi che viene utilizzato a livello globale e non generare più eventi a causa di un errore interno) e tutte le richieste future potrebbero fallire o comportarsi in modo folle

Esempio di codice: decidere se arrestarsi in modo anomalo

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

Citazione del blog: "Esistono tre scuole di pensiero sulla gestione degli errori" (Dal blog jsrecipes)

... Esistono principalmente tre scuole di pensiero sulla gestione degli errori: 1. Lascia che l'applicazione si blocchi e riavvii. 2. Gestisci tutti i possibili errori e non andare mai in crash. 3. Approccio equilibrato tra i due


Numero 7: utilizzare un logger maturo per aumentare la visibilità degli errori

TL; DR: un set di strumenti di registrazione avanzati come Winston, Bunyan o Log4J, accelererà la scoperta e la comprensione degli errori. Quindi dimentica di console.log.

Altrimenti: scorrere attraverso console.logs o manualmente attraverso file di testo disordinato senza strumenti di query o un visualizzatore di log decente potrebbe tenervi occupati sul lavoro fino a tardi

Esempio di codice: logger Winston in azione

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

Citazione del blog: "Identifichiamo alcuni requisiti (per un logger):" (Dal blog strongblog)

... Consente di identificare alcuni requisiti (per un logger): 1. Timestamp per ciascuna riga del registro. Questo è abbastanza autoesplicativo - dovresti essere in grado di dire quando si è verificata ogni voce di registro. 2. Il formato di registrazione dovrebbe essere facilmente digeribile dall'uomo e dalle macchine. 3. Consente più flussi di destinazione configurabili. Ad esempio, potresti scrivere registri di traccia in un file, ma quando si verifica un errore, scrivere nello stesso file, quindi nel file di errore e inviare un'e-mail contemporaneamente ...


Numero 8: Scopri errori e tempi di inattività utilizzando i prodotti APM

TL; DR: i prodotti di monitoraggio e prestazioni (noti anche come APM) misurano in modo proattivo la tua base di codice o API in modo che possano evidenziare automaticamente magicamente errori, arresti anomali e parti lente che ti mancavano

Altrimenti: potresti dedicare grandi sforzi alla misurazione delle prestazioni dell'API e dei tempi di inattività, probabilmente non sarai mai consapevole di quali parti del codice siano più lente nello scenario del mondo reale e di come queste influiscono sulla UX

Citazione del blog: "Segmenti di prodotti APM" (Dal blog Yoni Goldberg)

"... I prodotti APM costituiscono 3 segmenti principali: 1. Monitoraggio di siti Web o API - servizi esterni che monitorano costantemente i tempi di attività e le prestazioni tramite richieste HTTP. Possono essere configurati in pochi minuti. Di seguito sono riportati alcuni contendenti selezionati: Pingdom, Uptime Robot e New Relic 2 Strumentazione del codice - famiglia di prodotti che richiedono di incorporare un agente nell'applicazione per beneficiare del rilevamento del codice lento, delle statistiche delle eccezioni, del monitoraggio delle prestazioni e molto altro Di seguito sono riportati alcuni contendenti selezionati: New Relic, App Dynamics 3. Dashboard di intelligence operativa - questa linea di prodotti si concentra sull'agevolazione del team operativo con metriche e contenuti curati che aiutano a rimanere facilmente al top delle prestazioni dell'applicazione. Ciò comporta di solito l'aggregazione di più fonti di informazioni (registri delle applicazioni, registri dei DB, registri dei server, ecc.) e la progettazione del cruscotto iniziale. Di seguito sono riportati alcuni contendenti selezionati: Datadog, Splunk "


Quanto sopra è una versione abbreviata - vedi qui altre best practice ed esempi


30

È possibile rilevare eccezioni non rilevate, ma è di utilità limitata. Vedi http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions:4c933d54-1428-443c-928d-4e1ecbdd56cb

monit, foreverO upstartpuò essere usato per riavviare processo nodo quando si blocca. Un arresto gradevole è la cosa migliore che si possa sperare (ad es. Salvare tutti i dati in memoria nel gestore eccezioni non rilevati).


4
+1 Il link è utile, grazie. Sto ancora cercando la migliore pratica e il significato di "riavvio grazioso" nel contesto di node.js
momo,

La mia comprensione del "grazioso riavvio" in questo contesto sarebbe essenzialmente ciò che suggerisce nponeccop: lasciare che il processo muoia e lasciare che tutto ciò che lo sta eseguendo in primo luogo lo riavvii.
Ilkka,

Grazie mille per quel link! Davvero utile!
SatheeshJM,

Questa è un'ottima risposta Tuttavia, non sono d'accordo sulla restituzione di un errore nel tuo primo esempio. Restituire un Errorrende il valore di ritorno polimorfico che confonde inutilmente la semantica della funzione. Inoltre, le immersioni da 0 è già gestito in JavaScript, dando Infinity, -Infinityo NaN, i valori in cui typeof === 'number'. Possono essere verificati con !isFinite(value). In generale, consiglierei di non restituire mai un errore da una funzione. Meglio in termini di leggibilità del codice e manutenzione per generare o restituire un valore speciale non polimorfico con semantica coerente.
wprl

Il collegamento è interrotto. downforeveryoneorjustme.com/debuggable.com
Kev

13

I domini nodejs sono il modo più aggiornato di gestire gli errori in nodejs. I domini possono acquisire sia errori / altri eventi sia oggetti tradizionalmente generati. I domini forniscono anche funzionalità per la gestione di callback con un errore passato come primo argomento tramite il metodo intercetta.

Come nella normale gestione degli errori in stile try / catch, in genere è preferibile generare errori quando si verificano e bloccare le aree in cui si desidera isolare gli errori dal resto del codice. Il modo per "bloccare" queste aree è chiamare domain.run con una funzione come blocco di codice isolato.

Nel codice sincrono, quanto sopra è sufficiente: quando si verifica un errore o lo lasci passare, oppure lo catturi e lo gestisci lì, ripristinando tutti i dati che devi ripristinare.

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

Quando l'errore si verifica in un callback asincrono, è necessario essere in grado di gestire completamente il rollback dei dati (stato condiviso, dati esterni come database, ecc.). O devi impostare qualcosa per indicare che si è verificata un'eccezione - dovunque ti interessi a quel flag, devi aspettare che il callback venga completato.

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
  }

})});

Parte di quel codice sopra è brutto, ma puoi creare modelli per te stesso per renderlo più bello, ad esempio:

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

AGGIORNAMENTO (2013-09):

Sopra, uso un futuro che implica la semantica delle fibre , che ti consente di aspettare i futures in linea. Questo in realtà ti consente di utilizzare i tradizionali blocchi di prova per tutto - che trovo essere il modo migliore per andare. Tuttavia, non puoi sempre farlo (ad esempio nel browser) ...

Ci sono anche futures che non richiedono la semantica delle fibre (che quindi funzionano con il normale browser JavaScript). Questi possono essere chiamati futures, promesse o differiti (da qui in poi farò riferimento ai futures). Le librerie di futures JavaScript semplici consentono di propagare gli errori tra futures. Solo alcune di queste librerie consentono di gestire correttamente qualsiasi futuro lanciato, quindi attenzione.

Un esempio:

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

Questo imita un normale tentativo di cattura, anche se i pezzi sono asincroni. Stampa:

1
2
handler

Si noti che non stampa "3" perché è stata generata un'eccezione che interrompe quel flusso.

Dai un'occhiata alle promesse di bluebird:

Nota che non ho trovato molte altre librerie diverse da queste che gestiscono correttamente le eccezioni generate. jQuery è rinviato, ad esempio, non lo fa: il gestore "fail" non farebbe mai in modo che l'eccezione venga lanciata in un gestore "then", che secondo me è un affare.


La specifica delle promesse appropriate in Javascript è nota come Promises / A +. È possibile visualizzare un elenco di implementazioni qui: github.com/promises-aplus/promises-spec/blob/master/… . Si noti che un semplice Promises / A + è inutilizzabile in pratica - Promises / A + lascia ancora molti problemi pratici per le biblioteche per risolversi. Tuttavia, sono assolutamente garantiti elementi assolutamente essenziali come la propagazione dell'errore che mostri, l'ordine di esecuzione deterministica e la sicurezza dall'overflow dello stack.
Esailija,


11

Ne ho scritto di recente su http://snmaynard.com/2012/12/21/node-error-handling/ . Una nuova funzionalità del nodo nella versione 0.8 sono i domini e consentono di combinare tutte le forme di gestione degli errori in un unico modulo di gestione più semplice. Puoi leggere su di loro nel mio post.

Puoi anche usare qualcosa come Bugsnag per tracciare le tue eccezioni non rilevate ed essere avvisato via e-mail, chat o creare un biglietto per un'eccezione non rilevata (sono il co-fondatore di Bugsnag).


2
Il modulo di dominio è ora ufficialmente deprecato. nodejs.org/api/domain.html
MattSidor

3

Vorrei solo aggiungere che la libreria Step.js ti aiuta a gestire le eccezioni passando sempre alla funzione del passaggio successivo. Pertanto è possibile disporre come ultima funzione di una funzione che controlla eventuali errori in uno dei passaggi precedenti. Questo approccio può semplificare notevolmente la gestione degli errori.

Di seguito una citazione dalla pagina di github:

eventuali eccezioni generate vengono rilevate e passate come primo argomento alla funzione successiva. Finché non annidate le funzioni di callback in linea con le vostre funzioni principali, ciò impedisce che si verifichino eccezioni non rilevate. Questo è molto importante per i server node.JS a esecuzione prolungata poiché un'unica eccezione non rilevata può arrestare l'intero server.

Inoltre, è possibile utilizzare Step per controllare l'esecuzione degli script per avere una sezione di cleanup come ultimo step. Ad esempio, se si desidera scrivere uno script di build in Node e segnalare quanto tempo è stato necessario per scrivere, l'ultimo passaggio può essere eseguito (anziché tentare di estrarre l'ultimo callback).


3

Un'istanza in cui l'uso di un try-catch potrebbe essere appropriato è quando si utilizza un ciclo forEach. È sincrono ma allo stesso tempo non è possibile utilizzare solo un'istruzione return nell'ambito interno. È invece possibile utilizzare un approccio try and catch per restituire un oggetto Error nell'ambito appropriato. Prendere in considerazione:

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

È una combinazione degli approcci descritti da @balupton sopra.


Invece di generare errori, alcuni sviluppatori consigliano di utilizzare il concetto Risultato di Rust per restituire un OK o un Errore , quando l'errore è una possibilità nota. Ciò mantiene gli errori separati da errori imprevisti. Un'implementazione di JS di questo è r-risultato .
Joeytwiddle,

È una decisione di progettazione a livello di app. Penso che il tuo concetto di restituzione degli errori sia approssimativamente equivalente e semplice da iniziare (senza dipendenze extra), ma meno esplicito (il risultato ti rende dolorosamente consapevole quando potrebbe essere necessario gestire i guasti) e meno efficiente in quei casi in cui uno stack è costruito inutilmente.
Joeytwiddle,

1

Dopo aver letto questo post qualche tempo fa, mi chiedevo se fosse sicuro usare domini per la gestione delle eccezioni a livello di API / funzione. Volevo usarli per semplificare il codice di gestione delle eccezioni in ogni funzione asincrona che ho scritto. La mia preoccupazione era che l'uso di un nuovo dominio per ciascuna funzione avrebbe comportato un notevole sovraccarico. I miei compiti sembrano indicare che c'è un sovraccarico minimo e che le prestazioni sono effettivamente migliori con i domini rispetto a provare a catturare in alcune situazioni.

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


1

Gli errori di cattura sono stati discussi molto bene qui, ma vale la pena ricordare di disconnettersi da qualche parte in modo da poterli visualizzare e sistemare le cose.

Bunyan è un popolare framework di registrazione per NodeJS - supporta la scrittura in un sacco di diversi punti di output che lo rende utile per il debug locale, purché si eviti console.log. Nel gestore degli errori del tuo dominio potresti sputare l'errore in un file di registro.

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

Questo può richiedere molto tempo se hai molti errori e / o server da controllare, quindi potrebbe valere la pena esaminare uno strumento come Raygun (disclaimer, lavoro a Raygun) per raggruppare gli errori o usarli entrambi insieme. Se hai deciso di utilizzare Raygun come strumento, è anche abbastanza facile da configurare

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

Incrociata con l'utilizzo di uno strumento come PM2 o ​​per sempre, l'app dovrebbe essere in grado di arrestarsi in modo anomalo, disconnettersi da ciò che è successo e riavviare senza problemi importanti.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.