Суть проблемы
Для примера возьмём простое приложение на 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/
Добавить комментарий