Обработка ошибок в асинхронных вызовах node.js
Я новичок в node.js, хотя я довольно хорошо знаком с JavaScript в целом. Мой вопрос касается "лучших практик" в том, как обрабатывать ошибки в node.js.
Обычно при программировании веб-серверов, серверов FastCGI или веб-страниц на разных языках я использую исключения с блокирующими обработчиками в многопоточной среде. Когда приходит запрос, я обычно делаю что-то вроде этого:
function handleRequest(request, response) {
try {
if (request.url=="whatever")
handleWhateverRequest(request, response);
else
throw new Error("404 not found");
} catch (e) {
response.writeHead(500, {'Content-Type': 'text/plain'});
response.end("Server error: "+e.message);
}
}
function handleWhateverRequest(request, response) {
if (something)
throw new Error("something bad happened");
Response.end("OK");
}
Таким образом, я всегда могу обрабатывать внутренние ошибки и отправлять действительный ответ пользователю.
Я понимаю, что с node.js предполагается использовать неблокирующие вызовы, что, очевидно, приводит к разному количеству обратных вызовов, как в этом примере:
var sys = require('sys'),
fs = require('fs');
require("http").createServer(handleRequest).listen(8124);
function handleRequest(request, response) {
fs.open("/proc/cpuinfo", "r",
function(error, fd) {
if (error)
throw new Error("fs.open error: "+error.message);
console.log("File open.");
var buffer = new require('buffer').Buffer(10);
fs.read(fd, buffer, 0, 10, null,
function(error, bytesRead, buffer) {
buffer.dontTryThisAtHome(); // causes exception
response.end(buffer);
}); //fs.read
}); //fs.open
}
Этот пример полностью уничтожит сервер, потому что исключения не вылавливаются.
Моя проблема заключается в том, что я больше не могу использовать один try/catch и, таким образом, вообще не поймаю ошибок, которые могут возникнуть при обработке запроса.
Конечно, я мог бы добавить try/catch в каждом обратном вызове, но мне не нравится этот подход, потому что тогда он до программиста, что он не забывает попробовать/поймать. Для сложного сервера с множеством разных и сложных обработчиков это неприемлемо.
Я мог бы использовать глобальный обработчик исключений (предотвращая полный сбой сервера), но затем я не могу отправить ответ пользователю, так как я не знаю, какой запрос приведет к исключению. Это также означает, что запрос остается необработанным/открытым, и браузер ждет навсегда ответа.
Есть ли у кого-то хорошее твердое решение?
Ответы
Ответ 1
Node 0,8 вводит новую концепцию под названием "Домены". Они очень похожи на AppDomains в .net и обеспечивают способ инкапсуляции группы операций ввода-вывода. Они в основном позволяют вам обернуть вызовы обработки запросов в группе, специфичной для контекста. Если эта группа выбрасывает любые неперехваченные исключения, их можно обрабатывать и обрабатывать таким образом, чтобы вы могли получить доступ ко всей требуемой области и конкретной конкретной информации для успешного восстановления после ошибки (если это возможно).
Эта функция новая и только что была введена, поэтому используйте ее с осторожностью, но из того, что я могу сказать, она была специально введена для решения проблемы, которую пытается решить OP.
Документацию можно найти по адресу: http://nodejs.org/api/domain.html
Ответ 2
Оформить обработчик uncaughtException
в node.js. Он захватывает сброшенные ошибки, которые выходят за пределы цикла событий.
http://nodejs.org/docs/v0.4.7/api/process.html#event_uncaughtException_
Но не бросать ошибки всегда лучше. Вы можете просто сделать return res.end('Unabled to load file xxx');
Ответ 3
Это одна из проблем с Node прямо сейчас. Практически невозможно отследить, какой запрос вызвал ошибку, вызванную обратным вызовом.
Вам придется обрабатывать свои ошибки в самих обратных вызовах (где у вас все еще есть ссылка на объекты запроса и ответа), если это возможно. Обработчик uncaughtException остановит процесс Node от выхода, но запрос, который вызвал исключение, в первую очередь будет просто висеть там с точки зрения пользователя.
Ответ 4
Я даю ответ на свой вопрос...:)
Как кажется, нет возможности вручную обманывать ошибки. Теперь я использую вспомогательную функцию, которая сама возвращает функцию, содержащую блок try/catch. Кроме того, мой собственный класс веб-сервера проверяет, вызывает ли функция обработки запроса response.end()
или вспомогательная функция try/catch waitfor()
(в противном случае возникает исключение). Это позволяет избежать того, что запрос ошибочно оставлен незащищенным разработчиком. Это не 100% -ное решение с ошибкой, но для меня достаточно хорошо.
handler.waitfor = function(callback) {
var me=this;
// avoid exception because response.end() won't be called immediately:
this.waiting=true;
return function() {
me.waiting=false;
try {
callback.apply(this, arguments);
if (!me.waiting && !me.finished)
throw new Error("Response handler returned and did neither send a "+
"response nor did it call waitfor()");
} catch (e) {
me.handleException(e);
}
}
}
Таким образом, мне просто нужно добавить встроенный вызов waitfor()
, чтобы быть в безопасности.
function handleRequest(request, response, handler) {
fs.read(fd, buffer, 0, 10, null, handler.waitfor(
function(error, bytesRead, buffer) {
buffer.unknownFunction(); // causes exception
response.end(buffer);
}
)); //fs.read
}
Фактический механизм проверки немного сложнее, но должно быть ясно, как это работает. Если кто-то заинтересован, я могу опубликовать полный код здесь.
Ответ 5
Очень хороший вопрос. Я имею дело с той же проблемой сейчас. Вероятно, лучшим способом было бы использовать uncaughtException
. Ссылка на объекты respone и request не является проблемой, потому что вы можете обернуть их в свой объект исключения, который передается в событие uncaughtException
. Что-то вроде этого:
var HttpException = function (request, response, message, code) {
this.request = request;
this.response = response;
this.message = message;
this.code = code || 500;
}
Бросьте его:
throw new HttpException(request, response, 'File not found', 404);
И обработайте ответ:
process.on('uncaughtException', function (exception) {
exception.response.writeHead(exception.code, {'Content-Type': 'text/html'});
exception.response.end('Error ' + exception.code + ' - ' + exception.message);
});
Я еще не тестировал это решение, но я не вижу причин, по которым это не сработало.
Ответ 6
Одна идея: вы можете просто использовать вспомогательный метод для создания обратных вызовов и использовать его в своей стандартной практике. Это накладывает нагрузку на разработчика, но по крайней мере у вас может быть "стандартный" способ обработки ваших обратных вызовов, так что вероятность его забывания низкая:
var callWithHttpCatch = function(response, fn) {
try {
fn && fn();
}
catch {
response.writeHead(500, {'Content-Type': 'text/plain'}); //No
}
}
<snipped>
var buffer = new require('buffer').Buffer(10);
fs.read(fd, buffer, 0, 10, null,
function(error, bytesRead, buffer) {
callWithHttpCatch(response, buffer.dontTryThisAtHome()); // causes exception
response.end(buffer);
}); //fs.read
}); //fs.open
Я знаю, что, вероятно, это не тот ответ, который вы искали, но одна из приятных вещей о ECMAScript (или функциональном программировании в целом) заключается в том, как легко вы можете использовать свой собственный инструмент для таких вещей.
Ответ 7
Во время написания этого подхода, я вижу, это использование "Promises".
http://howtonode.org/promises
https://www.promisejs.org/
Они позволяют хорошо структурировать код и обратные вызовы для управления ошибками, а также сделать его более читабельным.
В основном он использует функцию .then().
someFunction().then(success_callback_func, failed_callback_func);
Вот базовый пример:
var SomeModule = require('someModule');
var success = function (ret) {
console.log('>>>>>>>> Success!');
}
var failed = function (err) {
if (err instanceof SomeModule.errorName) {
// Note: I've often seen the error definitions in SomeModule.errors.ErrorName
console.log("FOUND SPECIFIC ERROR");
}
console.log('>>>>>>>> FAILED!');
}
someFunction().then(success, failed);
console.log("This line with appear instantly, since the last function was asynchronous.");
Ответ 8
Две вещи действительно помогли мне решить эту проблему в моем коде.
- Модуль 'longjohn', который позволяет видеть полную трассировку стека (через несколько асинхронных обратных вызовов).
-
Простая техника закрытия для хранения исключений в стандартной идиоме callback(err, data)
(показана здесь в CoffeeScript).
ferry_errors = (callback, f) ->
return (a...) ->
try f(a...)
catch err
callback(err)
Теперь вы можете обернуть небезопасный код, и ваши обратные вызовы обрабатывают ошибки одинаково: проверяя аргумент ошибки.
Ответ 9
Недавно я создал простую абстракцию с именем WaitFor для вызова асинхронных функций в режиме синхронизации (на основе Fibers): https://github.com/luciotato/waitfor
Это слишком ново, чтобы быть "твердым".
используя wait.for, вы можете использовать async-функцию, как если бы они были синхронизированы, без блокировки цикла событий node. Это почти то же самое, что вы привыкли:
var wait=require('wait.for');
function handleRequest(request, response) {
//launch fiber, keep node spinning
wait.launchFiber(handleinFiber,request, response);
}
function handleInFiber(request, response) {
try {
if (request.url=="whatever")
handleWhateverRequest(request, response);
else
throw new Error("404 not found");
} catch (e) {
response.writeHead(500, {'Content-Type': 'text/plain'});
response.end("Server error: "+e.message);
}
}
function handleWhateverRequest(request, response, callback) {
if (something)
throw new Error("something bad happened");
Response.end("OK");
}
Поскольку вы находитесь в волокне, вы можете последовательно программировать "блокировку волокна", но не node цикл событий.
Другой пример:
var sys = require('sys'),
fs = require('fs'),
wait = require('wait.for');
require("http").createServer( function(req,res){
wait.launchFiber(handleRequest,req,res) //handle in a fiber
).listen(8124);
function handleRequest(request, response) {
try {
var fd=wait.for(fs.open,"/proc/cpuinfo", "r");
console.log("File open.");
var buffer = new require('buffer').Buffer(10);
var bytesRead=wait.for(fs.read,fd, buffer, 0, 10, null);
buffer.dontTryThisAtHome(); // causes exception
response.end(buffer);
}
catch(err) {
response.end('ERROR: '+err.message);
}
}
Как вы можете видеть, я использовал wait.for для вызова асинхронных функций node в режиме синхронизации,
без видимых обратных вызовов, поэтому я могу иметь весь код внутри одного блока try-catch.
wait.for выдаст исключение, если какая-либо из функций async вернет err! == null
Дополнительная информация на https://github.com/luciotato/waitfor
Ответ 10
Также в синхронном многопоточном программировании (например,.NET, Java, PHP) вы не можете вернуть какую-либо значимую информацию клиенту, когда будет обнаружено пользовательское исключение Unkown. Вы можете просто вернуть HTTP 500, когда у вас нет информации об исключении.
Таким образом, "секрет" заключается в заполнении описательного объекта Error, таким образом ваш обработчик ошибок может отображать значимую ошибку в правильном состоянии HTTP +, необязательно, описательный результат. Однако вы также должны поймать исключение, прежде чем оно поступит в process.on('uncaughtException'):
Шаг1: определение значимого объекта ошибки
function appError(errorCode, description, isOperational) {
Error.call(this);
Error.captureStackTrace(this);
this.errorCode = errorCode;
//...other properties assigned here
};
appError.prototype.__proto__ = Error.prototype;
module.exports.appError = appError;
Шаг 2: при выбросе исключения заполните его свойствами (см. шаг 1), который позволяет обработчику преобразовать его в HTTP-результат meannigul:
throw new appError(errorManagement.commonErrors.resourceNotFound, "further explanation", true)
Шаг 3: При вызове некоторого потенциально опасного кода, уловить ошибки и повторно выбросить эту ошибку при заполнении дополнительных контекстных свойств в объекте Error
Шаг 4: вы должны поймать исключение во время обработки запроса. Это проще, если вы используете какую-то ведущую библиотеку promises (BlueBird отлично), которая позволяет вам улавливать асинхронные ошибки. Если вы не можете использовать promises, чем любая встроенная библиотека NODE вернет ошибки в обратном вызове.
Шаг 5. Теперь, когда ваша ошибка поймана и содержит описательную информацию о том, что происходит, вам нужно только сопоставить ее с значимым ответом HTTP. Приятная часть здесь состоит в том, что у вас может быть централизованный, единственный обработчик ошибок, который получает все ошибки и сопоставляет их с ответом HTTP:
//this specific example is using Express framework
res.status(getErrorHTTPCode(error))
function getErrorHTTPCode(error)
{
if(error.errorCode == commonErrors.InvalidInput)
return 400;
else if...
}
Вы можете использовать другие соответствующие передовые методы здесь