Cara Membangun Sistem Penanganan Kesalahan Node.js

Diterbitkan: 2022-03-11

Tidak sulit untuk melihat bahwa beberapa orang berjuang untuk menangani kesalahan, dan beberapa bahkan benar-benar melewatkannya. Menangani kesalahan dengan benar berarti tidak hanya mengurangi waktu pengembangan dengan menemukan bug dan kesalahan dengan mudah, tetapi juga mengembangkan basis kode yang kuat untuk aplikasi skala besar.

Secara khusus, pengembang Node.js terkadang menemukan diri mereka bekerja dengan kode yang tidak terlalu bersih saat menangani berbagai jenis kesalahan, salah menerapkan logika yang sama di mana-mana untuk menanganinya. Mereka terus bertanya pada diri sendiri "Apakah Node.js buruk dalam menangani kesalahan?" atau Jika tidak, bagaimana cara menanganinya?” Jawaban saya kepada mereka adalah “Tidak, Node.js tidak buruk sama sekali. Itu tergantung pada kami para pengembang.”

Berikut adalah salah satu solusi favorit saya untuk itu.

Jenis Kesalahan di Node.js

Pertama-tama, perlu memiliki pemahaman yang jelas tentang kesalahan di Node.js. Secara umum, kesalahan Node.js dibagi menjadi dua kategori berbeda: kesalahan operasional dan kesalahan programmer .

  • Kesalahan operasional mewakili masalah runtime yang hasilnya diharapkan dan harus ditangani dengan cara yang tepat. Kesalahan operasional tidak berarti aplikasi itu sendiri memiliki bug, tetapi pengembang harus menanganinya dengan cermat. Contoh kesalahan operasional termasuk "kehabisan memori", "input yang tidak valid untuk titik akhir API", dan seterusnya.
  • Kesalahan programmer mewakili bug yang tidak terduga dalam kode yang ditulis dengan buruk. Mereka berarti kode itu sendiri memiliki beberapa masalah untuk dipecahkan dan dikodekan dengan salah. Contoh yang baik adalah mencoba membaca properti "tidak terdefinisi." Untuk memperbaiki masalah ini, kode harus diubah. Itu adalah bug yang dibuat oleh pengembang, bukan kesalahan operasional.

Dengan mengingat hal itu, Anda seharusnya tidak memiliki masalah dalam membedakan dua kategori kesalahan ini: Kesalahan operasional adalah bagian alami dari sebuah aplikasi, dan kesalahan pemrogram adalah bug yang disebabkan oleh pengembang. Pertanyaan logis berikut ini adalah: “Mengapa berguna untuk membaginya menjadi dua kategori dan menanganinya?”

Tanpa pemahaman yang jelas tentang kesalahan, Anda mungkin merasa ingin memulai ulang aplikasi setiap kali terjadi kesalahan. Apakah masuk akal untuk memulai ulang aplikasi karena kesalahan "File Tidak Ditemukan" ketika ribuan pengguna menikmati aplikasi? Sama sekali tidak.

Tapi bagaimana dengan kesalahan programmer? Apakah masuk akal untuk menjaga aplikasi tetap berjalan ketika bug yang tidak diketahui muncul yang dapat mengakibatkan efek bola salju yang tidak terduga dalam aplikasi? Sekali lagi, pasti tidak!

Saatnya Menangani Kesalahan dengan Benar

Dengan asumsi Anda memiliki pengalaman dengan JavaScript asinkron dan Node.js, Anda mungkin mengalami kekurangan saat menggunakan panggilan balik untuk menangani kesalahan. Mereka memaksa Anda untuk memeriksa kesalahan sampai ke kesalahan bersarang, menyebabkan masalah "panggilan balik neraka" yang membuat sulit untuk mengikuti aliran kode.

Menggunakan janji atau async/menunggu adalah pengganti yang baik untuk panggilan balik. Alur kode khas async/await terlihat seperti berikut:

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

Menggunakan objek Error bawaan Node.js adalah praktik yang baik karena mencakup informasi yang intuitif dan jelas tentang kesalahan seperti StackTrace, yang sebagian besar pengembang andalkan untuk melacak akar kesalahan. Dan properti tambahan yang bermakna seperti kode status HTTP dan deskripsi dengan memperluas kelas Error akan membuatnya lebih informatif.

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

Saya hanya menerapkan beberapa kode status HTTP demi kesederhanaan, tetapi Anda bebas untuk menambahkan lebih banyak nanti.

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

Tidak perlu memperluas BaseError atau APIError, tetapi tidak apa-apa untuk memperluasnya untuk kesalahan umum sesuai dengan kebutuhan dan preferensi pribadi Anda.

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

Jadi bagaimana Anda menggunakannya? Lemparkan saja ini:

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

Penanganan Kesalahan Node.js Terpusat

Sekarang, kami siap untuk membangun komponen utama dari sistem penanganan kesalahan Node.js kami: komponen penanganan kesalahan terpusat.

Biasanya merupakan ide yang baik untuk membangun komponen penanganan kesalahan terpusat untuk menghindari kemungkinan duplikasi kode saat menangani kesalahan. Komponen penanganan kesalahan bertugas membuat kesalahan yang tertangkap dapat dimengerti dengan, misalnya, mengirim pemberitahuan ke admin sistem (jika perlu), mentransfer peristiwa ke layanan pemantauan seperti Sentry.io, dan mencatatnya.

Berikut adalah alur kerja dasar untuk menangani kesalahan:

Penanganan kesalahan di Node.js: alur kerja dasar

Di beberapa bagian kode, kesalahan ditangkap untuk ditransfer ke middleware penanganan kesalahan.

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

Middleware penanganan kesalahan adalah tempat yang baik untuk membedakan antara jenis kesalahan dan mengirimkannya ke komponen penanganan kesalahan terpusat. Mengetahui dasar-dasar tentang penanganan kesalahan di middleware Express.js pasti akan membantu.

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

Sekarang, orang dapat membayangkan seperti apa komponen terpusat itu karena kita telah menggunakan beberapa fungsinya. Ingatlah bahwa itu sepenuhnya terserah Anda bagaimana menerapkannya, tetapi mungkin terlihat seperti berikut:

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

Terkadang, output dari "console.log" default membuat sulit untuk melacak kesalahan. Sebaliknya, akan jauh lebih baik untuk mencetak kesalahan dengan cara yang diformat sehingga pengembang dapat dengan cepat memahami masalah dan memastikannya telah diperbaiki.

Secara keseluruhan, ini akan menghemat waktu pengembang sehingga memudahkan untuk melacak kesalahan dan menanganinya dengan meningkatkan visibilitasnya. Ini adalah keputusan yang baik untuk menggunakan logger yang dapat disesuaikan seperti winston atau morgan.

Berikut adalah logger winston yang disesuaikan:

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

Apa yang pada dasarnya disediakan adalah masuk ke berbagai level dengan cara yang diformat, dengan warna yang jelas, dan masuk ke media keluaran yang berbeda sesuai dengan lingkungan runtime. Hal yang baik dengan ini adalah Anda dapat menonton dan meminta log dengan menggunakan API bawaan winston. Selanjutnya, Anda dapat menggunakan alat analisis log untuk menganalisis file log yang diformat untuk mendapatkan informasi yang lebih berguna tentang aplikasi. Ini luar biasa, bukan?

Sampai saat ini, kita kebanyakan membahas berurusan dengan kesalahan operasional. Bagaimana dengan kesalahan programmer? Cara terbaik untuk mengatasi kesalahan ini adalah dengan segera crash dan memulai ulang dengan anggun dengan restart otomatis seperti PM2—alasannya adalah bahwa kesalahan programmer tidak terduga, karena itu adalah bug aktual yang dapat menyebabkan aplikasi berakhir dalam keadaan dan perilaku yang salah. dengan cara yang tidak terduga.

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

Last but not least, saya akan menyebutkan berurusan dengan penolakan dan pengecualian janji yang tidak tertangani.

Anda mungkin menemukan diri Anda menghabiskan banyak waktu berurusan dengan janji ketika bekerja pada aplikasi Node.js/Express. Tidak sulit untuk melihat pesan peringatan tentang penolakan janji yang tidak tertangani saat Anda lupa menangani penolakan.

Pesan peringatan tidak banyak membantu kecuali mencatat, tetapi praktik yang baik adalah menggunakan fallback yang layak dan berlangganan process.on('unhandledRejection', callback) .

Alur penanganan kesalahan tipikal mungkin terlihat seperti berikut:

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

Membungkus

Ketika semua dikatakan dan dilakukan, Anda harus menyadari bahwa penanganan kesalahan bukanlah tambahan opsional melainkan bagian penting dari sebuah aplikasi, baik dalam tahap pengembangan dan produksi.

Strategi penanganan kesalahan dalam satu komponen di Node.js akan memastikan pengembang menghemat waktu yang berharga dan menulis kode yang bersih dan dapat dipelihara dengan menghindari duplikasi kode dan konteks kesalahan yang hilang.

Saya harap Anda menikmati membaca artikel ini dan menemukan alur kerja penanganan kesalahan dan implementasi yang dibahas bermanfaat untuk membangun basis kode yang kuat di Node.js.


Bacaan Lebih Lanjut di Blog Teknik Toptal:

  • Menggunakan Rute Express.js untuk Penanganan Kesalahan Berbasis Janji