146

处理JavaScript异常的正确姿势 | Fundebug博客

 6 years ago
source link: https://blog.fundebug.com/2017/11/27/proper-error-handling-javascript/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

处理JavaScript异常的正确姿势

译者按: 错误是无法避免的,妥善处理它才是最重要的!

本文采用意译,版权归原作者所有

如果你相信墨菲定律的话,任何事情如果会出问题,那么就一定会出问题。对于代码,即使我们有 100%的自信没有问题,依然有可能出问题。在这篇文章,我们来研究如何处理 JavaScript 的错误。我会先介绍坏的处理方式、好的处理方式,最终介绍异步代码和 Ajax。

个人感觉,事件驱动的编程设计使得 JavaScript 语言非常的丰富灵活。我们设想浏览器就是事件驱动机器,错误同样由它的驱动产生。当一个错误触发,导致某个事件被抛出。从理论上说,错误在 JavaScript 中就是事件。

如果你对此感到陌生,那么暂且不管它。在这篇文章中,我主要关注浏览器端的 JavaScript。

这篇文章基于JavaScript 中的错误处理部分的概念。如果你还不熟悉,我建议你先阅读一下。

Demo 演示

我们使用的 Demo 可以在GitHub下载,程序运行起来会呈现如下页面:

demo.png

所有的按钮都会触发错误,抛出TypeError。下面是该模块的定义:

// scripts/error.js

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

error()中定义了一个空对象foo,因此调用foo.bar()会因为未被定义而报错。我们使用单元测试来验证一下:

// tests/scripts/errorTest.js

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

我们使用了Mocha配合Should.js做单元测试。

当你克隆了代码库并安装了依赖包以后,你可以使用 npm t 来执行测试。当然,你也可以执行某个测试文件,比如:./node_modules/mocha/bin/mocha tests/scripts/errorTest.js

相信我,像 JavaScript 这样的动态语言来说,不管谁都很容易遇到这样的错误。

坏的处理方式

我已经将按钮对应的处理事件函数抽象得简单一点,如下所示:

// scripts/badHandler.js

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

badHandler接收一个fn作为回调函数,该回调函数在badHandler中被调用。我们编写相应的单元测试:

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

你会发现,如果出现异常,badHandler只是简单的返回null。如果配合完整的代码,你会发现问题所在:

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

如果出错的时候将其 try-catch,然后仅仅返回null,我根本找不到哪里出错了。这种安静失败(fail-silent)策略可能导致 UI 紊乱也可能导致数据错乱,并且在 Debug 的时候可能花了几个小时却忽略了 try-catch 里面的代码才是致祸根源。如果代码复杂到有多层次的调用,简直不可能找到哪里出了错。因此,我们不建议使用安静失败策略,我们需要更加优雅的方式。

不坏但很烂的方式

// scripts/uglyHandler.js

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

它处理错误的方式是抓到错误e,然后抛出一个新的错误。这样做的确优于之前安静失败的策略。如果出了错,我可以一层层找回去,直到找到原本抛出的错误e。简单的抛出一个Error('a new 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);
});

获取堆栈信息

你可以将错误信息发送到服务器:

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

为了获取更详细的报错信息,并且省去处理数据的麻烦,你也可以使用 fundebug 的JavaScript 监控插件三分钟快速接入 bug 监控服务。

下面是服务器接收到的报错消息:

log_on_server.png

如果你的脚本是放在另一个域名下,如果你不开启CORS,除了Script error.,你将看不到任何有用的报错信息。如果想知道具体解法,请参考:Script error.全面解析

异步错误处理

由于setTimeout异步执行,下面的代码异常将不会被try...catch捕获:

// scripts/asyncHandler.js

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

try...catch语句只会捕获当前执行环境下的异常。但是在上面异常抛出的时候,JavaScript 解释器已经不在try...catch中了,因此无法被捕获。所有的 Ajax 请求也是这样。

我们可以稍微改进一下,将try...catch写到异步函数的回调中:

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

不过,这样的套路会导致项目中充满了try...catch,代码非常不简洁。并且,执行 JavaScript 的 V8 引擎不鼓励在函数中使用try...catch。好在,我们不需要这么做,全局的错误处理onerror会捕获这些错误。

我的建议是不要隐藏错误,勇敢地抛出来。没有人会因为代码出现 bug 导致程序崩溃而羞耻,我们可以让程序中断,让用户重来。错误是无法避免的,如何去处理它才是最重要的。

关于Fundebug

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了70亿+错误事件,付费客户有阳光保险、达令家、核桃编程、荔枝FM、微脉等众多品牌企业。欢迎大家免费试用

您的用户遇到BUG了吗?

体验Demo 免费使用

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK