JavaScript中正确错误处理指南

嗯,JavaScript中错误处理的危险。 如果您相信墨菲定律 ,那么任何可能出错的地方都会出错。 在本文中,我想探讨JavaScript中的错误处理。 我将介绍一些陷阱,良好实践,并以异步代码和Ajax结尾。

该热门文章已于08.06.2017更新,以解决读者的反馈。 具体来说,将文件名添加到代码片段中,清理单元测试,将包装模式添加到uglyHandler ,添加有关CORS和第三者错误处理程序的部分。

我觉得JavaScript的事件驱动范例为语言增加了丰富性。 我喜欢将浏览器想象成事件驱动的机器,并且错误没有什么不同。 当发生错误时,某个事件会被抛出。 从理论上讲,人们可以说错误是JavaScript中的简单事件。

如果对您来说这听起来很陌生,请在旅行时系好安全带,好好兜风。 对于本文,我将只关注客户端JavaScript。

本主题基于JavaScript中的异常处理中介绍的概念。 如果您不熟悉,我建议您阅读基础知识。 本文还假设您具有JavaScript的中级知识。 如果您想升级,为什么不注册SitePoint Premium并观看我们的课程JavaScript:下一步 。 第一课是免费的。

无论哪种情况,我的目标都是探索处理异常的绝对必要。 阅读本文会使您在下次看到一个不错的try...catch块时再三考虑。

演示

我们将在本文中使用的演示可在GitHub找到 ,并提供如下页面:

JavaScript演示中的错误处理

单击所有按钮都会引爆“炸弹”。 该炸弹模拟一个引发为TypeError 。 以下是此类模块的定义:

// scripts/error.js

function error() {
  var foo = {};
  return foo.bar();
}

首先,此函数声明一个名为foo的空对象。 请注意, bar()不会在任何地方获得定义。 让我们验证一下,它将通过良好的单元测试引爆炸弹:

// tests/scripts/errorTest.js

it('throws a TypeError', function () {
  should.throws(error, TypeError);
});

此单元测试在Mocha中 ,在Should.js中具有测试断言。 Mocha是测试运行程序,而Should.js是断言库。 如果您还不熟悉,可以随时探索测试API。 测试以it('description')开始,并以should通过/失败通过。 单元测试在Node上运行,不需要浏览器。 我建议注意测试,因为它们会证明纯JavaScript的关键概念。

克隆存储库并安装依赖项后,即可使用npm t运行测试。 或者,您可以像这样运行此单个测试: ./node_modules/mocha/bin/mocha tests/scripts/errorTest.js

如图所示, error()定义了一个空对象,然后尝试访问一个方法。 由于bar()在对象中不存在,因此它将引发异常。 相信我,使用像JavaScript这样的动态语言,这会发生在每个人身上!

坏人

关于一些错误的错误处理。 我已经从实现中抽象了按钮上的处理程序。 处理程序如下所示:

// scripts/badHandler.js

function badHandler(fn) {
  try {
    return fn();
  } catch (e) { }
  return null;
}

该处理程序接收fn回调作为参数。 然后在处理程序函数中调用此回调。 单元测试显示了它的有用性:

// tests/scripts/badHandlerTest.js

it('returns a value without errors', function() {
  var fn = function() {
    return 1;
  };

  var result = badHandler(fn);

  result.should.equal(1);
});

it('returns a null with errors', function() {
  var fn = function() {
    throw new Error('random error');
  };

  var result = badHandler(fn);

  should(result).equal(null);
});

如您所见,如果出现问题,此错误的错误处理程序将返回null 。 回调fn()可以指向合法方法或炸弹。

下面的click事件处理程序讲述了故事的其余部分:

// scripts/badHandlerDom.js

(function (handler, bomb) {
  var badButton = document.getElementById('bad');

  if (badButton) {
    badButton.addEventListener('click', function () {
      handler(bomb);
      console.log('Imagine, getting promoted for hiding mistakes');
    });
  }
}(badHandler, error));

可恶的是我只能得到一个null 。 当我尝试找出问题出在哪里时,这使我失明。 这种无故障的策略的范围从不良的UX一直到数据损坏。 令人沮丧的是,我可以花几个小时来调试症状,但是错过了try-catch块。 这个邪恶的处理程序吞噬了代码中的错误,并假装一切都很好。 对于不提高代码质量的组织来说,这可能没关系。 但是,隐藏错误将使您在未来数小时内进行调试。 在具有深层调用堆栈的多层解决方案中,不可能弄清楚哪里出了问题。 至于错误处理,这是非常糟糕的。

静默失败策略将使您为更好的错误处理而苦恼。 JavaScript提供了一种更优雅的异常处理方式。

丑陋的

是时候调查一个丑陋的处理程序了。 我将跳过与DOM紧密耦合的部分。 与您看到的错误处理程序没有什么区别。

// scripts/uglyHandler.js

function uglyHandler(fn) {
  try {
    return fn();
  } catch (e) {
    throw new Error('a new error');
  }
}

重要的是它处理异常的方式,如下面的单元测试所示:

// tests/scripts/uglyHandlerTest.js

it('returns a new error with errors', function () {
  var fn = function () {
    throw new TypeError('type error');
  };

  should.throws(function () {
    uglyHandler(fn);
  }, Error);
});

对不良处理程序的绝对改进。 在这里,异常通过调用堆栈冒泡。 我现在喜欢的是错误会解开堆栈 ,这对于调试非常有帮助。 除了一个例外,解释器会在堆栈中向上移动以寻找另一个处理程序。 这为处理调用堆栈顶部的错误提供了许多机会。 不幸的是,由于它是一个丑陋的处理程序,因此我丢失了原始错误。 因此,我被迫遍历堆栈以找出原始异常。 至少我知道,出了点问题,这就是为什么您抛出异常。

或者,可以用自定义错误结束丑陋的处理程序。 当您将更多详细信息添加到错误中时,它不再很丑陋,但会有所帮助。 关键是要附加有关该错误的特定信息。

例如:

// scripts/specifiedError.js

// Create a custom error
var SpecifiedError = function SpecifiedError(message) {
  this.name = 'SpecifiedError';
  this.message = message || '';
  this.stack = (new Error()).stack;
};

SpecifiedError.prototype = new Error();
SpecifiedError.prototype.constructor = SpecifiedError;
// scripts/uglyHandlerImproved.js

function uglyHandlerImproved(fn) {
  try {
    return fn();
  } catch (e) {
    throw new SpecifiedError(e.message);
  }
}
// tests/scripts/uglyHandlerImprovedTest.js

it('returns a specified error with errors', function () {
  var fn = function () {
    throw new TypeError('type error');
  };

  should.throws(function () {
    uglyHandlerImproved(fn);
  }, SpecifiedError);
});

指定的错误会添加更多详细信息,并保留原始错误消息。 有了这一改进,它不再是一个丑陋的处理程序,而是干净而有用的。

有了这些处理程序,我仍然遇到未处理的异常。 让我们看看浏览器是否有能力解决这个问题。

放松堆栈

因此,消除异常的一种方法是在调用堆栈的顶部放置try...catch

例如说:

function main(bomb) {
  try {
    bomb();
  } catch (e) {
    // Handle all the error things
  }
}

但是,还记得我说过浏览器是事件驱动的吗? 是的,JavaScript中的异常仅是一个事件。 解释器在正在执行的上下文中暂停执行并展开。 事实证明,我们可以使用一个onerror全局事件处理程序

它是这样的:

// scripts/errorHandlerDom.js

window.addEventListener('error', function (e) {
  var error = e.error;
  console.log(error);
});

该事件处理程序捕获任何执行上下文中的错误。 错误事件会因各种错误而从各种目标触发。 如此根本的事情是该事件处理程序将错误处理集中在代码中。 与任何其他事件一样,您可以使用菊花链处理程序来处理特定的错误。 如果遵循SOLID原则,这将使错误处理程序具有单一目的。 这些处理程序可以随时注册。 解释器将根据需要循环使用尽可能多的处理程序。 代码库从try...catch块中解放出来,这些块遍历了所有的块,这使得调试变得很容易。 关键是要像JavaScript中的事件处理那样对待错误处理。

既然有了一种使用全局处理程序释放堆栈的方法,我们该怎么办?

毕竟,调用堆栈可能会与您同在。

捕获堆栈

调用堆栈对于解决问题很有帮助。 好消息是浏览器提供了开箱即用的信息。 堆栈属性不是标准的一部分,但在最新的浏览器中始终可用。

因此,例如,您现在可以在服务器上记录错误:

// scripts/errorAjaxHandlerDom.js

window.addEventListener('error', function (e) {
  var stack = e.error.stack;
  var message = e.error.toString();

  if (stack) {
    message += '\n' + stack;
  }

  var xhr = new XMLHttpRequest();
  xhr.open('POST', '/log', true);
  // Fire an Ajax request with error details
  xhr.send(message);
});

从此示例可能并不明显,但这将与前面的示例一起触发。 每个错误处理程序都可以有一个单一目的,即保持代码DRY

在浏览器中,事件处理程序将附加到DOM。 这意味着,如果您正在构建第三方库,则事件将与客户端代码共存。 window.addEventListener()会为您解决此问题,它不会清除现有事件。

以下是此日志在服务器上的屏幕截图:

到节点服务器的Ajax日志请求

该日志位于命令提示符内,是的,它确实在Windows上运行。

此消息来自Firefox Developer Edition54。使用正确的错误处理程序,请注意,问题出在哪里很清楚。 无需掩饰错误,通过浏览此内容,我可以看到引发异常的原因和位置。 这种透明级别对于调试前端代码非常有用。 您可以分析日志,以了解哪些条件触发了哪些错误。

调用堆栈对于调试很有帮助,永远不要低估调用堆栈的功能。

一个陷阱是,如果您具有来自其他域的脚本并启用CORS,您将看不到任何错误详细信息。 例如,当您将脚本放在CDN上时,就会发生这种情况,以利用每个域六个请求的限制。 e.message只会说“脚本错误”,这很不好。 在JavaScript中,错误信息仅适用于单个域。

一种解决方案是在保留错误消息的同时重新引发错误:

try {
  return fn();
} catch (e) {
  throw new Error(e.message);
}

一旦将错误重新抛出,您的全局错误处理程序将完成其余的工作。 仅确保您的错误处理程序在同一域中。 您甚至可以将其包装在具有特定错误信息的自定义错误周围。 这将保留原始消息,堆栈和自定义错误对象。

异步处理

啊,异步的危险。 JavaScript将异步代码从执行上下文中剥离。 这意味着下面的异常处理程序有问题:

// scripts/asyncHandler.js

function asyncHandler(fn) {
  try {
    // This rips the potential bomb from the current context
    setTimeout(function () {
      fn();
    }, 1);
  } catch (e) { }
}

单元测试讲述了故事的其余部分:

// tests/scripts/asyncHandlerTest.js

it('does not catch exceptions with errors', function () {
  // The bomb
  var fn = function () {
    throw new TypeError('type error');
  };

  // Check that the exception is not caught
  should.doesNotThrow(function () {
    asyncHandler(fn);
  });
});

没有捕获到异常,我可以使用此单元测试来验证。 请注意,尽管我将代码包裹在一个不错的try...catch周围,​​但是发生了未处理的异常。 是的, try...catch语句仅在单个执行上下文中起作用。 等到引发异常时,解释器已经离开了try...catch 。 Ajax调用也会发生相同的行为。

因此,一种替代方法是在异步回调中捕获异常:

setTimeout(function () {
  try {
    fn();
  } catch (e) {
    // Handle this async error
  }
}, 1);

这种方法可以工作,但是还有很多改进的余地。 首先, try...catch块到处都是纠结。 实际上,在1970年代,糟糕的程序被调用了,他们希望代码返回。 另外,V8引擎不建议在函数内部使用try ... catch块 。 V8是Chrome浏览器和Node中使用的JavaScript引擎。 一种想法是将块移到调用堆栈的顶部,但这不适用于异步代码。

那么,这导致我们走向何方? 我说全局错误处理程序可以在任何执行上下文中运行是有原因的。 如果将错误处理程序添加到窗口对象,就完成了! 保持DRY和SOLID的决定很有意义,这很高兴。 全局错误处理程序将使您的异步代码保持整洁。

以下是此异常处理程序在服务器上报告的内容。 请注意,如果您遵循此步骤,则看到的输出将根据所使用的浏览器而有所不同。

服务器上的异步错误报告

该处理程序甚至告诉我该错误来自异步代码。 它说它来自setTimeout()函数。 太酷了!

结论

在错误处理领域,至少有两种方法。 一种是静默的方法,您可以忽略代码中的错误。 另一种是快速失败的方法,其中错误使世界停滞并倒退。 我认为我很赞成这两个,为什么。 我的看法:不要掩饰问题。 没有人会因为程序中可能发生的事故而感到羞耻。 可以停止,倒带并再次尝试给用户。

在一个远非完美的世界中,重要的是要有第二次机会。 错误是不可避免的,重要的是您对它们的处理。

这篇文章是由同行评审添Severien莫里茨克罗格 感谢所有SitePoint的同行评审人员使SitePoint内容达到最佳状态!

From: https://www.sitepoint.com/proper-error-handling-javascript/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值