JavaScript 错误处理最佳实践:从基础到异步

作者:半夏

时间:2026-10-30

浏览:552

本文深入探讨 JavaScript 中的错误处理机制,涵盖从基础异常捕获到异步代码处理的完整实践。文章分析了“失败即沉默”和“静默抛出”等常见误区,推荐利用全局 error 事件监听器集中处理异常,并结合调用栈信息上报服务器。针对异步场景,解释了 try-catch 的局限性,并提供了在回调或 Promise 中正确处理错误的方案,旨在帮助开发者提升代码健壮性与调试效率。

文章目录

JavaScript 错误处理概述

无论开发者的技术水平如何,错误或异常都是应用开发中不可避免的一部分。Web 开发环境的复杂性导致了许多错误发生的可能性和现实性。解决这些问题的关键在于妥善处理任何不可预见或可预见的错误,从而掌控用户体验。在 JavaScript 中,有多种技术和语言特性可用于正确解决各类问题。

在 JavaScript 中处理错误颇具挑战性。如果相信墨菲定律,那么会出错的地方终究会出错。本文将深入探讨 JavaScript 中的错误处理机制,涵盖其中的陷阱与最佳实践,并最终讨论异步代码及 Ajax 的处理方式。

示例

本文使用的代码示例托管在 GitHub 上,当前页面展示如下:

JavaScript 错误处理概述
 

单击每个按钮都会触发一个错误,模拟产生一个 TypeError 类型的异常。以下是对该模块的定义及其单元测试代码。


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

首先,该函数定义了一个空对象 foo。请注意,bar() 方法并未在任何地方定义。我们通过单元测试来验证这确实会引发报错。


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

该单元测试使用了 Mocha 和 Should.js 库中的测试断言。Mocha 是一个测试运行框架,should.js 是一个断言库。如果你对这些工具不熟悉,可以在线免费查阅它们的文档。一个测试用例通常以 it('description') 开始,以 should 断言的通过或失败结束。使用这套框架的好处在于可以在 Node.js 环境中进行单元测试,而无需局限于浏览器环境。建议大家认真对待这些测试,因为它们验证了 JavaScript 中许多关键的基本概念。

如上所示,error() 函数定义了一个空对象,随后试图调用其中的方法。由于该对象中不存在 bar() 方法,它会抛出一个异常。在 JavaScript 这种动态语言中,任何人都可能犯此类错误。

不良的错误处理示范

不良示范

首先来看一种不佳的错误处理方式。我将处理错误的动作抽象出来,并绑定在按钮上。以下是该处理程序的单元测试示例:


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

该处理函数接收一个回调函数 fn 作为依赖,并在处理程序内部调用该函数。这个单元测试示例展示了如何使用此方法。


it('returns a value without errors', function() {
 var fn = function() {
  return 1;
 };
 var result = target(fn);
 result.should.equal(1);
});

it('returns a null with errors', function() {
 var fn = function() {
  throw Error('random error');
 };
 var result = target(fn);
 should(result).equal(null);
});

正如你所见,如果发生错误,这个奇怪的处理方法会返回 null。回调函数 fn() 指向一个合法的方法或错误。下面的单击处理事件完成了剩余部分。


(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。这让我在确定究竟发生了什么错误时感到迷茫。这种发生错误即沉默的策略覆盖了从用户体验设计到数据损坏的各个环节。随之而来的令人沮丧的一面是,我必须花费好几个小时调试却看不到 try-catch 代码块里的错误。这种奇怪的处理隐藏了代码中所有的报错,它假设一切正常。这在某些不注重代码质量的团队中可能顺利执行,但这些被隐藏的错误最终会迫使你花几个小时来调试代码。在一种依赖于调用栈的多层解决方案中,有可能确定错误来源。可能在极少数情况下对 try-catch 做故障静默处理是合适的,但如果遇到错误就去处理,也不是一个好方案。

这种失败即沉默的策略促使你在代码中对错误做更好的处理。JavaScript 提供了更优雅的方式来处理这类问题。

不易读的错误处理方案

不易读的方案

继续,接下来看看不太好理解的处理方式。我将跳过与 DOM 紧耦合的部分。这部分与我们刚刚看过的不好的处理方式没什么不同。重点是下面单元测试中处理异常的部分。


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

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

比起刚才不好的处理方式,有一个很好的进步。异常在调用堆栈中被抛出。我喜欢的地方是错误从堆栈中解放出来,这对于调试有巨大的帮助。抛出一个异常,解释器就会在调用堆栈中一级级查看找到下一个处理函数。这就提供了很多机会在调用堆栈的顶层去处理错误。不幸的是,因为他是一种不太好理解的错误,我看不到了原始错误的信息。所以我必须沿着调用栈找过去,找到最原始的异常。但是至少我知道抛出异常的地方发生了一个错误。

这种不易读的错误处理虽然无伤大雅但是却使得代码难以理解。让我们看看浏览器如何处理错误的。

利用调用栈进行调试

调用栈

那么,抛出异常的一种方式就是在调用堆栈的顶层添加 try...catch 代码块。比如说:


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

但是,记得我说过浏览器是事件驱动的吗?是的,JavaScript 中的一个异常不过就是一个事件。解释器会在发生异常当前的上下文处停止程序,并抛出异常。为了证实这一点,下面写了一个我们能够看到的全局的事件处理函数 onerror。它看上去就是这个样子:


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

这个事件处理函数在执行环境中捕获错误。错误事件会在各种各样的地方产生各种错误。这种方式的重点是在代码中集中处理错误。就像其他的事件一样,你可以用一个全局的处理函数去处理各种不同的错误。这使得错误处理只有一个单一的目标,如果你遵守 SOLID (single responsibility 单一职责, open-closed 开闭, Liskov substitution 代换, interface segregation 界面分离 and dependency inversion 依赖倒置) 原则。你可以在任何时候注册错误处理函数。解释器会循环执行这些函数。代码从充满 try...catch 的语句中解放出来,变得易于调试。这种做法的关键是像处理 JavaScript 普通事件一样处理发生的错误。

现在,有了一种方法,用全局处理函数来显示出调用栈,我们可以用它来做什么?终究,我们要利用调用栈。

记录下调用栈

调用栈在处理修复 bug 上非常有用。好消息是浏览器提供了这个信息。就算目前,error 对象的 stack 属性并不是标准,但是在比较新的浏览器里都普遍支持这个属性。

所以,我们能够做的很酷的事情就是把它给服务器打印出来:


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);
 xhr.send(message);
});

在代码示例中可能不太明显,但这个事件处理程序会被前面的错误代码触发。如上所述,每个处理程序都有一个单一的目的,它使代码 DRY(don't repeat yourself 不重复制造轮子)。我感兴趣的是如何在服务器上捕获这些消息。

下面是 node 运行时的截图:

JavaScript 错误处理概述
 

调用堆栈对调试代码很有帮助。永远不要低估调用栈的作用。

异步代码的错误处理

异步处理

哦,处理异步代码相当危险!JavaScript 将异步代码从当前的执行环境中带出来。这意味着下面这种 try...catch 语句有个问题。


function asyncHandler(fn) {
 try {
  setTimeout(function () {
   fn();
  }, 1);
 } catch (e) { }
}

这个单元测试还有剩下的部分:


it('does not catch exceptions with errors', function () {
 var fn = function () {
  throw new TypeError('type error');
 };
 failedPromise(function() {
  target(fn);
 }).should.be.rejectedWith(TypeError);
});

function failedPromise(fn) {
 return new Promise(function(resolve, reject) {
  reject(fn);
 });
}

我必须用一个 promise 来结束这个处理程序,以验证异常。注意,尽管我的代码都在 try...catch 中,但是还是出现了未处理的异常。是的,try...catch 只在一个单独的执行环境中有作用。当异常被抛出时,解释器的执行环境已经不是当前的 try-catch 块了。这一行为的发生与 Ajax 调用相似。所以,现在有了两种选择。一种可选方案就是在异步回调中捕捉异常:


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

这种方法虽然有用,但是还有很大的提升空间。首先,try...catch 代码块在代码中处处出现。事实上,上世纪 70 年代编程调用,他们希望他们的代码能够回退。另外,V8 引擎不鼓励在函数中使用 try…catch 代码块 (V8 是 Chrome 浏览器和 Node 使用的 JavaScript 引擎)。他们推荐在调用堆栈顶层写这些捕获异常的代码块。

所以,这告诉我们什么?我上面说过的,在任何执行上下文中的全局错误处理程序是有必要的。如果你将一个错误处理程序添加到 window 对象,那就是说,您已经完成了!遵守 DRY 和 SOLID 的原则不是很好吗?一个全局错误处理程序将保持你的代码易读和干净。

下面就是服务器端异常处理打印的报告。注意,如果你使用的示例中的代码,输出的内容可能会根据你使用的浏览器不同有少许不同。


 JavaScript 错误处理概述

这个处理函数甚至可以告诉我哪个错误是出自于异步代码。它告诉我错误来自于 setTimeout() 处理函数。太酷了!

错误是每一个应用程序的一部分,但是适当的错误处理却不是。在处理错误这件事上至少有两种方法。一种是失败即沉默的方案,即在代码中忽略错误。另一种是快速发现和解决错误的方法,即在错误处停止并且重现。我想我已经把我赞成哪一种及为什么赞成表达地很清楚。我的选择:不要隐藏问题。没有人会为你程序中的意外事件去指责你。这是可以接受的,去打断点、重现、给用户一个尝试。在一个并不完美的世界中,给自己一个机会是很重要的。错误是不可避免的,为了解决错误你做的事情才是重要的。合理地运用JavaScript的错误处理特色和自动灵活的译码可以使用户的体验更顺畅,同时也让开发方的诊断工作变得更轻松。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章