try/catch 是安全网,不是安全带
当一行 JavaScript 代码抛出错误时,执行会立刻停止,错误沿着调用栈向上传递。如果没有任何代码捕获它,程序就会崩溃(在 Node 中),或者在控制台里打出一大片红字(在浏览器中)。try/catch 就是拦截它的方式:意思是“我知道这里可能失败,失败了我要这样处理”。
基本结构如下:
JSON.parse 抛出了一个 SyntaxError。执行立即跳到 catch 代码块,错误绑定到 err 上。第三个 console.log 依然会执行,崩溃被控制住了。
如果 try 代码块成功执行、没有抛出错误,catch 代码块就会被完全跳过。它只为失败的情况而存在。
错误对象
抛出的任何东西都会绑定到 catch (...) 中的参数名上。通常它是一个 Error 实例,有三个有用的字段:
name 告诉你它属于哪个子类(TypeError、RangeError、SyntaxError 等,下一篇文档会详细介绍)。message 是给人看的错误描述。stack 是完整的调用栈,调试时非常宝贵。
有一个坑:JavaScript 允许你 throw 任何东西,而不只是 Error 对象。老代码里有时会写 throw "something broke"。你自己写 throw 时,一定要抛出 Error,这样调用者才能拿到调用栈:
finally 无论如何都会执行
finally 是可选的第三个代码块,无论是否抛出错误、catch 是否处理了错误,它都会执行。它用于清理工作:关闭文件、释放锁、隐藏加载动画:
无论加载成功还是失败,加载动画都会被隐藏。如果没有 finally,你就得在两个分支里各写一遍这行代码,而且很可能在其中一个分支里忘掉。
即使 try 或 catch 代码块里有 return,finally 也会执行。函数会在 finally 执行完之后才返回。这偶尔会让人意外,但大多数时候这正是你想要的。
不一定需要 catch
catch 是可选的。单独的 try/finally 是合法的,当你需要保证清理、又不打算处理错误(希望错误继续传递)时,它就很有用:
内层的 try/finally 在 fn() 抛出错误时也会释放锁,但不会吞掉错误,调用者仍然能看到它。悄悄吞掉错误(“出错了,但我谁也没告诉”)是最常见的调试噩梦之一。
重新抛出:处理一部分,其余向上传递
catch 代码块不必处理所有错误。你可以检查错误,处理合理的部分,把剩下的重新抛出:
instanceof 检查就是这种模式:识别出你知道如何恢复的错误,其他错误让它们继续沿调用栈向上传递。用空的 catch 代码块吞掉所有错误是一种代码坏味道:一旦出现意外,你就什么线索都没有了。
在 async/await 中使用 try/catch
在 async 函数中,被 await 的 Promise 如果 reject,就会变成抛出的错误,try/catch 处理它们的方式和同步错误完全一样:
有一个细节:你必须在 try 代码块内部 await 这个 Promise。如果你直接返回一个 Promise 而不等待它,reject 会在函数已经退出之后才发生,catch 永远看不到它:
async function bad() {
try {
return fetch("/broken"); // no await - caller sees the rejection
} catch (err) {
// never runs
}
}
经验法则:在 async 函数中,想让 try/catch 覆盖什么,就 await 什么。
嵌套 try/catch
当内层和外层代码会因为不同的原因失败、而你想分别处理时,可以嵌套 try/catch 代码块:
内层的 catch 处理“数据结构不对”的情况,返回一个安全的默认值。外层的处理“输入根本不是 JSON”的情况,把错误包装后重新抛出。当每一层都有不同的恢复策略时,嵌套没有问题;如果两个代码块做的事情一样,就把它们合并成一层。
什么时候不该用 try/catch
try/catch 是用来处理可预期、可恢复的失败的工具,不是用来掩盖 bug 的。
- 不要“以防万一”把整个函数体包起来。如果你对这个错误没有实际的处理方案,就让它向上传递:一个带调用栈的未捕获错误,比一个悄无声息的错误有用得多。
- 不要用它来控制流程。和
if判断相比,try代码块有实际的开销,也会让代码更混乱。if (user)比try { user.name } catch {}好。 - 不要捕获之后只打个日志就不管了。至少要重新抛出,或者返回一个调用者能识别的特殊值。
一个检验方法:“这里失败时,使用这段代码的人要怎么做?”如果你答不上来,那你还没准备好 catch 这个错误。
速查
try { ... } catch (err) { ... }:拦截抛出的错误。finally { ... }:始终执行,用于清理。throw new Error("..."):始终抛出Error的子类,这样调用栈才有用。- 在
catch中写throw err;:处理不了时重新抛出。 - 在
try中使用await:try/catch要捕获异步 reject,这一步必不可少。
下一节:错误类型
TypeError、RangeError、SyntaxError:JavaScript 有一系列内置的错误类,知道每一种代表什么,能让捕获和报告错误精确得多。这就是下一篇文档的内容。
常见问题
JavaScript 中的 try/catch 是如何工作的?
把可能出错的代码放进 try { ... }。如果里面有任何代码抛出错误,执行会直接跳到 catch (err) { ... } 代码块,抛出的值绑定到 err 上。如果没有抛出错误,就跳过 catch 代码块。可选的 finally { ... } 无论如何都会执行,适合做清理工作。
什么时候应该在 JavaScript 中使用 try/catch?
把它用在运行时确实可能失败的操作上:对不可信输入执行 JSON.parse、fetch 的响应、文件或网络 I/O。不要每一行都包起来:如果你没有恢复的方案,就让错误继续向上传递。在正常运行的代码外面套一个大范围的 try/catch,只会把 bug 藏起来,而不是处理它们。
try/catch 能捕获异步错误吗?
只有在 try 代码块里 await 这个 Promise 时才能。直接调用 somePromise() 不会被捕获,错误会变成未处理的 rejection。使用 async/await 时,try/catch 和同步代码中的用法完全一样。对于原始的 Promise,请在链上使用 .catch()。
如何在 JavaScript 中重新抛出错误?
在 catch 里直接写 throw err;(或者 throw 一个包装了它的新错误)。当你想处理一部分错误、让其他错误继续传递时,这很有用:先检查错误的类型或消息,处理能处理的,把剩下的重新抛出,让上层的调用者仍然能看到它们。