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