Cum să construiți un sistem de gestionare a erorilor Node.js

Publicat: 2022-03-11

Nu este greu de văzut că unii oameni se străduiesc să gestioneze erorile, iar unii chiar le lipsesc cu desăvârșire. Gestionarea corectă a erorilor înseamnă nu numai reducerea timpului de dezvoltare prin găsirea cu ușurință a erorilor și erorilor, ci și dezvoltarea unei baze de cod robuste pentru aplicații la scară largă.

În special, dezvoltatorii Node.js se trezesc uneori lucrând cu cod nu atât de curat în timp ce gestionează diverse tipuri de erori, aplicând incorect aceeași logică peste tot pentru a le trata. Ei continuă să se întrebe „Este Node.js prost în gestionarea erorilor?” sau dacă nu, cum să le gestionezi?” Răspunsul meu la ei este „Nu, Node.js nu este rău deloc. Asta depinde de noi, dezvoltatorii.”

Iată una dintre soluțiile mele preferate pentru asta.

Tipuri de erori în Node.js

În primul rând, este necesar să aveți o înțelegere clară a erorilor din Node.js. În general, erorile Node.js sunt împărțite în două categorii distincte: erori operaționale și erori ale programatorului .

  • Erorile operaționale reprezintă probleme de rulare ale căror rezultate sunt așteptate și trebuie tratate într-un mod adecvat. Erorile operaționale nu înseamnă că aplicația în sine are erori, dar dezvoltatorii trebuie să le gestioneze atent. Exemplele de erori operaționale includ „memorie lipsită”, „o intrare nevalidă pentru un punct final API” și așa mai departe.
  • Erorile programatorului reprezintă erori neașteptate în codul scris prost. Ele înseamnă că codul în sine are unele probleme de rezolvat și a fost codificat greșit. Un bun exemplu este să încercați să citiți o proprietate de „nedefinit”. Pentru a remedia problema, codul trebuie schimbat. Este o eroare creată de un dezvoltator, nu o eroare operațională.

Având în vedere acest lucru, nu ar trebui să aveți nicio problemă în a distinge aceste două categorii de erori: erorile operaționale sunt o parte naturală a unei aplicații, iar erorile programatorului sunt erori cauzate de dezvoltatori. O întrebare logică care urmează este: „De ce este util să le împărțim în două categorii și să le gestionăm?”

Fără o înțelegere clară a erorilor, este posibil să aveți chef să reporniți o aplicație ori de câte ori apare o eroare. Are sens să reporniți o aplicație din cauza erorilor „Fișier nu a fost găsit” atunci când mii de utilizatori se bucură de aplicație? Absolut nu.

Dar cum rămâne cu erorile programatorului? Are sens să mențineți o aplicație în funcțiune atunci când apare o eroare necunoscută care ar putea duce la un efect neașteptat de bulgăre de zăpadă în aplicație? Din nou, cu siguranță nu!

Este timpul să gestionați corect erorile

Presupunând că aveți o anumită experiență cu JavaScript asincron și Node.js, este posibil să fi întâmpinat dezavantaje atunci când utilizați apeluri inverse pentru a trata erorile. Ele vă obligă să verificați erorile până la cele imbricate, provocând probleme notorii de „callback hell” care fac dificilă urmărirea fluxului de cod.

Folosirea promisiunilor sau asincron/așteptă este un bun înlocuitor pentru apeluri inverse. Fluxul tipic de cod al async/wait arată astfel:

 const doAsyncJobs = async () => { try { const result1 = await job1(); const result2 = await job2(result1); const result3 = await job3(result2); return await job4(result3); } catch (error) { console.error(error); } finally { await anywayDoThisJob(); } }

Utilizarea obiectului Error incorporat Node.js este o practică bună, deoarece include informații intuitive și clare despre erori precum StackTrace, de care depind majoritatea dezvoltatorilor pentru a ține evidența rădăcinii unei erori. Și proprietăți suplimentare semnificative, cum ar fi codul de stare HTTP și o descriere prin extinderea clasei Error, o vor face mai informativă.

 class BaseError extends Error { public readonly name: string; public readonly httpCode: HttpStatusCode; public readonly isOperational: boolean; constructor(name: string, httpCode: HttpStatusCode, description: string, isOperational: boolean) { super(description); Object.setPrototypeOf(this, new.target.prototype); this.name = name; this.httpCode = httpCode; this.isOperational = isOperational; Error.captureStackTrace(this); } } //free to extend the BaseError class APIError extends BaseError { constructor(name, httpCode = HttpStatusCode.INTERNAL_SERVER, isOperational = true, description = 'internal server error') { super(name, httpCode, isOperational, description); } }

Am implementat doar câteva coduri de stare HTTP de dragul simplității, dar sunteți liber să adăugați mai multe mai târziu.

 export enum HttpStatusCode { OK = 200, BAD_REQUEST = 400, NOT_FOUND = 404, INTERNAL_SERVER = 500, }

Nu este nevoie să extindeți BaseError sau APIError, dar este în regulă să le extindeți pentru erori comune, în funcție de nevoile și preferințele dvs. personale.

 class HTTP400Error extends BaseError { constructor(description = 'bad request') { super('NOT FOUND', HttpStatusCode.BAD_REQUEST, true, description); } }

Deci cum îl folosești? Doar arunca asta in:

 ... const user = await User.getUserById(1); if (user === null) throw new APIError( 'NOT FOUND', HttpStatusCode.NOT_FOUND, true, 'detailed explanation' );

Gestionarea centralizată a erorilor Node.js

Acum, suntem gata să construim componenta principală a sistemului nostru de tratare a erorilor Node.js: componenta centralizată de gestionare a erorilor.

De obicei, este o idee bună să construiți o componentă centralizată de gestionare a erorilor pentru a evita posibilele duplicari de coduri atunci când se gestionează erori. Componenta de gestionare a erorilor este responsabilă să facă înțelese erorile detectate, de exemplu, trimițând notificări către administratorii de sistem (dacă este necesar), transferând evenimente la un serviciu de monitorizare precum Sentry.io și înregistrându-le.

Iată un flux de lucru de bază pentru tratarea erorilor:

Gestionarea erorilor în Node.js: flux de lucru de bază

În unele părți ale codului, erorile sunt capturate pentru a fi transferate într-un middleware de gestionare a erorilor.

 ... try { userService.addNewUser(req.body).then((newUser: User) => { res.status(200).json(newUser); }).catch((error: Error) => { next(error) }); } catch (error) { next(error); } ...

Middleware-ul de tratare a erorilor este un loc bun pentru a face distincția între tipurile de erori și a le trimite către componenta centralizată de tratare a erorilor. Cunoașterea elementelor de bază despre gestionarea erorilor în middleware Express.js ar ajuta cu siguranță.

 app.use(async (err: Error, req: Request, res: Response, next: NextFunction) => { if (!errorHandler.isTrustedError(err)) { next(err); } await errorHandler.handleError(err); });

Până acum, ne putem imagina cum ar trebui să arate componenta centralizată, deoarece am folosit deja unele dintre funcțiile sale. Rețineți că depinde în totalitate de dvs. cum să o implementați, dar ar putea arăta astfel:

 class ErrorHandler { public async handleError(err: Error): Promise<void> { await logger.error( 'Error message from the centralized error-handling component', err, ); await sendMailToAdminIfCritical(); await sendEventsToSentry(); } public isTrustedError(error: Error) { if (error instanceof BaseError) { return error.isOperational; } return false; } } export const errorHandler = new ErrorHandler();

Uneori, rezultatul implicit „console.log” face dificilă urmărirea erorilor. Mai degrabă, ar putea fi mult mai bine să tipăriți erorile într-un mod formatat, astfel încât dezvoltatorii să poată înțelege rapid problemele și să se asigure că sunt remediate.

În general, acest lucru va economisi timp dezvoltatorilor, facilitând urmărirea erorilor și gestionarea acestora prin creșterea vizibilității acestora. Este o decizie bună să angajați un logger personalizabil precum Winston sau Morgan.

Iată un logger Winston personalizat:

 const customLevels = { levels: { trace: 5, debug: 4, info: 3, warn: 2, error: 1, fatal: 0, }, colors: { trace: 'white', debug: 'green', info: 'green', warn: 'yellow', error: 'red', fatal: 'red', }, }; const formatter = winston.format.combine( winston.format.colorize(), winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.splat(), winston.format.printf((info) => { const { timestamp, level, message, ...meta } = info; return `${timestamp} [${level}]: ${message} ${ Object.keys(meta).length ? JSON.stringify(meta, null, 2) : '' }`; }), ); class Logger { private logger: winston.Logger; constructor() { const prodTransport = new winston.transports.File({ filename: 'logs/error.log', level: 'error', }); const transport = new winston.transports.Console({ format: formatter, }); this.logger = winston.createLogger({ level: isDevEnvironment() ? 'trace' : 'error', levels: customLevels.levels, transports: [isDevEnvironment() ? transport : prodTransport], }); winston.addColors(customLevels.colors); } trace(msg: any, meta?: any) { this.logger.log('trace', msg, meta); } debug(msg: any, meta?: any) { this.logger.debug(msg, meta); } info(msg: any, meta?: any) { this.logger.info(msg, meta); } warn(msg: any, meta?: any) { this.logger.warn(msg, meta); } error(msg: any, meta?: any) { this.logger.error(msg, meta); } fatal(msg: any, meta?: any) { this.logger.log('fatal', msg, meta); } } export const logger = new Logger();

Ceea ce oferă practic este înregistrarea la mai multe niveluri diferite într-un mod formatat, cu culori clare și conectarea la diferite medii de ieșire în funcție de mediul de rulare. Lucrul bun cu aceasta este că puteți urmări și interoga jurnalele folosind API-urile încorporate ale Winston. În plus, puteți utiliza un instrument de analiză a jurnalului pentru a analiza fișierele jurnal formatate pentru a obține mai multe informații utile despre aplicație. Este minunat, nu-i așa?

Până în acest moment, am discutat în cea mai mare parte despre abordarea erorilor operaționale. Ce zici de erorile de programare? Cea mai bună modalitate de a face față acestor erori este să vă blocați imediat și să reporniți grațios cu un repornitor automat precum PM2 - motivul fiind că erorile programatorului sunt neașteptate, deoarece sunt erori reale care ar putea face ca aplicația să ajungă într-o stare greșită și să se comporte. într-un mod neaşteptat.

 process.on('uncaughtException', (error: Error) => { errorHandler.handleError(error); if (!errorHandler.isTrustedError(error)) { process.exit(1); } });

Nu în ultimul rând, voi menționa abordarea respingerilor și excepțiilor de promisiuni nerezolvate.

S-ar putea să vă treziți petrecând mult timp ocupându-vă de promisiuni atunci când lucrați pe aplicațiile Node.js/Express. Nu este greu să vezi mesaje de avertizare despre respingerea promisiunilor nerezolvate atunci când uiți să gestionezi respingeri.

Mesajele de avertizare nu fac mare lucru, cu excepția înregistrării, dar este o practică bună să utilizați o rezervă decentă și să vă abonați la process.on('unhandledRejection', callback) .

Fluxul obișnuit de gestionare a erorilor poate arăta astfel:

 // somewhere in the code ... User.getUserById(1).then((firstUser) => { if (firstUser.isSleeping === false) throw new Error('He is not sleeping!'); }); ... // get the unhandled rejection and throw it to another fallback handler we already have. process.on('unhandledRejection', (reason: Error, promise: Promise<any>) => { throw reason; }); process.on('uncaughtException', (error: Error) => { errorHandler.handleError(error); if (!errorHandler.isTrustedError(error)) { process.exit(1); } });

Încheierea

Când totul este spus și gata, ar trebui să realizați că gestionarea erorilor nu este un plus opțional, ci mai degrabă o parte esențială a unei aplicații, atât în ​​etapa de dezvoltare, cât și în producție.

Strategia de gestionare a erorilor într-o singură componentă în Node.js va asigura dezvoltatorilor să economisească timp prețios și să scrie cod curat și care poate fi întreținut, evitând duplicarea codului și lipsa contextului de eroare.

Sper că v-a plăcut să citiți acest articol și că ați găsit fluxul de lucru și implementarea de tratare a erorilor discutate utile pentru construirea unei baze de cod robuste în Node.js.


Citiți suplimentare pe blogul Toptal Engineering:

  • Utilizarea rutelor Express.js pentru gestionarea erorilor bazată pe promisiuni