Обработка асинхронных ошибок с сохранением контекста запроса в connect/express

от автора

Те, кому приходилось разрабатывать более-менее большие web-проекты на node.js, наверняка сталкивались с проблемой обработки ошибок, произошедших внутри асинхронных вызовов. Эта проблема обычно всплывает далеко не сразу, а когда у вас уже есть много написанного кода, который делает нечто большее, чем выводит «Hello, World!».

Суть проблемы


Для примера возьмём простое приложение на connect:

var connect = require('connect');  var getName = function () { 	if (Math.random() > 0.5) { 		throw new Error('Can\'t get name'); 	} else { 		return 'World'; 	} };  var app = connect() 	.use(function (req, res, next) { 		try { 			var name = getName(); 			res.end('Hello, ' + name + '!'); 		} catch (e) { 			next(e); 		} 	}) 	.use(function (err, req, res, next) { 		res.end('Error: ' + err.message); 	});  app.listen(3000); 

Здесь мы имеем синхронную функцию, которая с некоторой долей вероятности, генерирует ошибку. Мы ловим эту ошибку и передаём в общий обработчик ошибок, который, в свою очередь, показывает ошибку пользователю. В данном примере вызов функции происходит синхронно и обработка ошибки ничем не отличается от подобной задачи в других языках.

Теперь попробуем сделать тоже самое, но функция getName будет асинхронной:

var connect = require('connect');  var getName = function (callback) { 	process.nextTick(function () { 		if (Math.random() > 0.5) { 			callback(new Error('Can\'t get name')); 		} else { 			callback(null, 'World'); 		} 	}); };  var app = connect() 	.use(function (req, res, next) { 		getName(function(err, name) { 			if (err) return next(err); 			res.end('Hello, ' + name + '!'); 		}); 	}) 	.use(function (err, req, res, next) { 		res.end('Error: ' + err.message); 	});  app.listen(3000); 

В этом примере мы уже не можем поймать ошибку через try/catch, т.к. она возникнет не во время вызова функции, а внутри асинхронного вызова, который произойдёт позже (в данном примере — на следующей итерации event loop). Поэтому мы использовали подход, рекомендованный разработчиками node.js — передаём ошибку в первом аргументе функции обратного вызова.

Такой подход полностью решает проблему обработки ошибок внутри асинхронных вызовов, но он сильно раздувает код, когда подобных вызовов становится много. В реальном приложении появляются много методов, которые вызывают друг-друга, могут иметь вложенные вызовы и быть частью цепочек асинхронных вызовов. И каждый раз при возникновении ошибки где-то в глубине стека вызовов нам необходимо «доставить» её на самый верх, там где мы можем её правильно обработать и сообщить пользователю о нештатной ситуации. В синхронном приложении за нас это делает try/catch — там мы можем выбросить ошибку внутри нескольких вложенных вызовов и поймать её там, где можем правильно обработать, без необходимости вручную передавать её наверх по стеку вызовов.

Решение

В Node.JS начиная с версии 0.8.0 появился механизм под названием Domain. Он позволяет отлавливать ошибки внутри асинхронных вызовов, при этом сохраняя контекст выполнения, в отличие от process.on(‘uncaughtException’). Думаю, пересказывать тут документацию по Domain смысла не имеет, т.к. механизм его работы довольно прост, поэтому я сразу перейду к конкретной реализации универсального обработчика ошибок для connect/express.

Connect/express заворачивает все middleware в блоки try/catch, поэтому, если вы делаете throw внутри middleware, ошибка будет передана в цепочку обработчиков ошибок (middleware с 4-мя аргументами на входе), а если таких middleware нет — в обработчик ошибок по умолчанию, который выведет trace ошибки в браузер и консоль. Но это поведение актуально только для ошибок произошедших в синхронном коде.

При помощи Domain мы можем перенаправлять ошибки, произошедшие внутри асинхронных вызовов, в контексте запроса в цепочку обработчиков ошибок этого запроса. Теперь для нас, в конечном итоге, обработка синхронных и асинхронных ошибок будет выглядеть одинаково.

Для этой цели я написал небольшой модуль-middleware для connect/express, который решает эту задачу. Модуль доступен на GitHub и в npm.

Пример использования:

var     connect = require('connect'),     connectDomain = require('connect-domain');  var app = connect()     .use(connectDomain())     .use(function(req, res){         if (Math.random() > 0.5) {             throw new Error('Simple error');         }         setTimeout(function() {             if (Math.random() > 0.5) {                 throw new Error('Asynchronous error from timeout');             } else {                 res.end('Hello from Connect!');             }         }, 1000);     })     .use(function(err, req, res, next) {         res.end(err.message);     });  app.listen(3000); 

В этом примере ошибки, выброшенные внутри синхронного и асинхронного вызова, будут обработаны одинаково. Вы можете выбросить ошибку на любой глубине вызовов в контексте запроса, и она будет обработана цепочкой обработчиков ошибок данного запроса.

var     connect = require('connect'),     connectDomain = require('connect-domain');  var app = connect()     .use(connectDomain())     .use(function(req, res){         if (Math.random() > 0.5) {             throw new Error('Simple error');         }         setTimeout(function() {             if (Math.random() > 0.5) {                 process.nextTick(function() {                     throw new Error('Asynchronous error from process.nextTick');                 });             } else {                 res.end('Hello from Connect!');             }         }, 1000);     })     .use(function(err, req, res, next) {         res.end(err.message);     });  app.listen(3000); 

В заключение отмечу, что официально стабильность модуля Domain на момент написания статьи остаётся экспериментальной, однако я уже использую описанный подход, хоть в небольшом но продакшене и не наблюдаю каких-либо проблем. Сайт, использующий данный модуль, ни разу не завершал работу аварийно и не страдает утечками памяти. Uptime процесса больше месяца.

ссылка на оригинал статьи http://habrahabr.ru/post/161889/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *