Node.js Hata İşleme Sistemi Nasıl Oluşturulur

Yayınlanan: 2022-03-11

Bazı insanların hatalarla başa çıkmakta zorlandıklarını ve hatta bazılarının bunu tamamen gözden kaçırdığını görmek zor değil. Hataları düzgün bir şekilde ele almak, yalnızca hataları ve hataları kolayca bularak geliştirme süresini kısaltmak değil, aynı zamanda büyük ölçekli uygulamalar için sağlam bir kod tabanı geliştirmek anlamına gelir.

Özellikle, Node.js geliştiricileri bazen kendilerini çok temiz olmayan kodlarla çalışırken, çeşitli türlerdeki hataları işlerken bulurlar ve bunlarla başa çıkmak için her yerde aynı mantığı yanlış bir şekilde uygularlar. Kendilerine "Node.js hataları işlemede kötü mü?" diye sorup duruyorlar. ya da değilse, bunlarla nasıl başa çıkılır?” Onlara cevabım “Hayır, Node.js hiç de fena değil. Bu, biz geliştiricilere bağlı.”

İşte bunun için en sevdiğim çözümlerden biri.

Node.js'deki Hata Türleri

Her şeyden önce, Node.js'deki hataları net bir şekilde anlamak gerekir. Genel olarak, Node.js hataları iki farklı kategoriye ayrılır: işlemsel hatalar ve programcı hataları .

  • Operasyonel hatalar , sonuçları beklenen ve uygun bir şekilde ele alınması gereken çalışma zamanı sorunlarını temsil eder. Operasyonel hatalar, uygulamanın kendisinde hatalar olduğu anlamına gelmez, ancak geliştiricilerin bunları dikkatli bir şekilde ele alması gerekir. İşlemsel hatalara örnek olarak "bellek yetersiz", "bir API uç noktası için geçersiz bir giriş" vb. verilebilir.
  • Programcı hataları , kötü yazılmış koddaki beklenmeyen hataları temsil eder. Kodun kendisinin çözmesi gereken bazı sorunları olduğu ve yanlış kodlandığı anlamına gelir. İyi bir örnek, “undefined” özelliğini okumaya çalışmaktır. Sorunu çözmek için kodun değiştirilmesi gerekiyor. Bu, bir geliştiricinin yaptığı bir hatadır, operasyonel bir hata değil.

Bunu göz önünde bulundurarak, bu iki hata kategorisini ayırt etmekte sorun yaşamazsınız: İşlemsel hatalar uygulamanın doğal bir parçasıdır ve programcı hataları geliştiricilerin neden olduğu hatalardır. Mantıklı bir soru şudur: “Onları iki kategoriye ayırıp onlarla uğraşmak neden faydalıdır?”

Hataları net bir şekilde anlamadan, bir hata oluştuğunda uygulamayı yeniden başlatmak isteyebilirsiniz. Binlerce kullanıcı uygulamadan faydalanırken “Dosya Bulunamadı” hataları nedeniyle bir uygulamayı yeniden başlatmak mantıklı mı? Kesinlikle hayır.

Peki ya programcı hataları? Uygulamada beklenmedik bir kartopu etkisine neden olabilecek bilinmeyen bir hata göründüğünde bir uygulamayı çalışır durumda tutmak mantıklı mı? Tekrar, kesinlikle hayır!

Hataları Doğru Şekilde Ele Alma Zamanı

Zaman uyumsuz JavaScript ve Node.js konusunda biraz deneyiminiz olduğunu varsayarsak, hatalarla başa çıkmak için geri aramaları kullanırken bazı dezavantajlar yaşamış olabilirsiniz. Sizi iç içe geçmiş hatalara kadar hataları kontrol etmeye zorlarlar ve kod akışını takip etmeyi zorlaştıran kötü şöhretli "geri arama cehennemi" sorunlarına neden olurlar.

Söz verme veya zaman uyumsuz/bekleme kullanmak, geri aramalar için iyi bir alternatiftir. async/await'in tipik kod akışı aşağıdaki gibi görünür:

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

Node.js yerleşik Error nesnesini kullanmak iyi bir uygulamadır, çünkü çoğu geliştiricinin bir hatanın kökünü takip etmek için bağımlı olduğu StackTrace gibi hatalar hakkında sezgisel ve net bilgiler içerir. HTTP durum kodu ve Error sınıfını genişleterek açıklama gibi ek anlamlı özellikler onu daha bilgilendirici hale getirecektir.

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

Basit olması için yalnızca bazı HTTP durum kodlarını uyguladım, ancak daha sonra daha fazlasını eklemekte özgürsünüz.

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

BaseError veya APIError'ı genişletmeye gerek yoktur, ancak ihtiyaçlarınıza ve kişisel tercihlerinize göre yaygın hatalar için genişletmenizde bir sakınca yoktur.

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

Peki nasıl kullanıyorsunuz? Sadece şunu at:

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

Merkezileştirilmiş Node.js Hata işleme

Artık Node.js hata işleme sistemimizin ana bileşenini oluşturmaya hazırız: merkezi hata işleme bileşeni.

Hataları işlerken olası kod tekrarlarını önlemek için merkezi bir hata işleme bileşeni oluşturmak genellikle iyi bir fikirdir. Hata işleme bileşeni, örneğin sistem yöneticilerine bildirim göndererek (gerekirse), olayları Sentry.io gibi bir izleme hizmetine aktararak ve bunları günlüğe kaydederek yakalanan hataları anlaşılır kılmaktan sorumludur.

İşte hatalarla başa çıkmak için temel bir iş akışı:

Node.js'de hata işleme: temel iş akışı

Kodun bazı bölümlerinde, hata işleme ara yazılımına aktarılmak üzere hatalar yakalanır.

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

Hata işleme ara yazılımı, hata türleri arasında ayrım yapmak ve bunları merkezi hata işleme bileşenine göndermek için iyi bir yerdir. Express.js ara yazılımındaki hataların ele alınmasıyla ilgili temel bilgileri bilmek kesinlikle yardımcı olacaktır.

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

Şimdiye kadar, merkezi bileşenin neye benzemesi gerektiğini hayal edebiliyoruz çünkü bazı fonksiyonlarını zaten kullandık. Nasıl uygulanacağının tamamen size bağlı olduğunu unutmayın, ancak aşağıdaki gibi görünebilir:

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

Bazen, varsayılan "console.log" çıktısı, hataları takip etmeyi zorlaştırır. Bunun yerine, geliştiricilerin sorunları hızlı bir şekilde anlayabilmeleri ve düzeltildiklerinden emin olabilmeleri için hataları biçimlendirilmiş bir şekilde yazdırmak çok daha iyi olabilir.

Genel olarak bu, geliştiricilere zamandan tasarruf sağlayarak hataları takip etmeyi ve görünürlüklerini artırarak bunlarla başa çıkmayı kolaylaştıracaktır. Winston veya morgan gibi özelleştirilebilir bir kaydedici kullanmak iyi bir karardır.

İşte özelleştirilmiş bir winston kaydedici:

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

Temel olarak sağladığı şey, açık renklerle biçimlendirilmiş bir şekilde birden çok farklı düzeyde oturum açmak ve çalışma zamanı ortamına göre farklı çıktı ortamlarında oturum açmaktır. Bununla ilgili iyi olan şey, winston'ın yerleşik API'lerini kullanarak günlükleri izleyebilmeniz ve sorgulayabilmenizdir. Ayrıca, uygulama hakkında daha yararlı bilgiler elde etmek için biçimlendirilmiş günlük dosyalarını analiz etmek için bir günlük analiz aracı kullanabilirsiniz. Harika, değil mi?

Bu noktaya kadar, çoğunlukla operasyonel hatalarla uğraşmayı tartıştık. Peki ya programcı hataları? Bu hatalarla başa çıkmanın en iyi yolu, PM2 gibi bir otomatik yeniden başlatıcı ile hemen çökmek ve zarif bir şekilde yeniden başlatmaktır; bunun nedeni, uygulamanın yanlış bir duruma gelmesine ve davranmasına neden olabilecek gerçek hatalar oldukları için programcı hatalarının beklenmedik olmasıdır. beklenmedik bir şekilde.

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

Son olarak, ele alınmayan söz retleri ve istisnalar ile uğraşmaktan bahsedeceğim.

Node.js/Express uygulamaları üzerinde çalışırken kendinizi vaatlerle uğraşmak için çok zaman harcarken bulabilirsiniz. Reddetme işlemlerini yapmayı unuttuğunuzda, işlenmeyen vaatlerin reddedildiğiyle ilgili uyarı mesajları görmek zor değildir.

Uyarı mesajları, günlüğe kaydetme dışında pek bir şey yapmaz, ancak uygun bir geri dönüş kullanmak ve process.on('unhandledRejection', callback) abone olmak iyi bir uygulamadır.

Tipik hata işleme akışı aşağıdaki gibi görünebilir:

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

Toplama

Her şey söylendiğinde ve yapıldığında, hata işlemenin isteğe bağlı bir ekstra değil, hem geliştirme aşamasında hem de üretimde bir uygulamanın önemli bir parçası olduğunu anlamalısınız.

Node.js'de hataları tek bir bileşende işleme stratejisi, geliştiricilerin değerli zamandan tasarruf etmelerini ve kod tekrarını ve eksik hata bağlamını önleyerek temiz ve bakımı yapılabilir kodlar yazmasını sağlar.

Umarım bu makaleyi okumaktan keyif almışsınızdır ve tartışılan hata işleme iş akışını ve uygulamasını Node.js'de sağlam bir kod tabanı oluşturmak için faydalı bulmuşsunuzdur.


Toptal Mühendislik Blogunda Daha Fazla Okuma:

  • Söze Dayalı Hata İşleme için Express.js Yollarını Kullanma