异步错误处理
浏览器中的异步包括定时器、事件、Promise 等。异步世界中的错误处理与同步世界中的处理不同。下面来看一些例子。
1、定时器的错误处理
之前我们介绍了如何使用 try/catch/finally 来处理错误,那异步中可以使用这些来处理错误吗?先来看一个例子:
function failAfterOneSecond() {setTimeout(() => {throw Error("Wrong!");}, 1000);
}
此函数在大约 1 秒后会抛出错误。那处理此异常的正确方法是什么?以下代码是无效的:
function failAfterOneSecond() {setTimeout(() => {throw Error("Wrong!");}, 1000);
}try {failAfterOneSecond();
} catch (error) {console.error(error.message);
}
我们知道,try/catch 是同步的,所以没办法这样来处理异步中的错误。当传递给 setTimeout 的回调运行时,try/catch 早已执行完毕。程序将会崩溃,因为未能捕获异常。它们是在两条路径上执行的:
A: --> try/catch
B: --> setTimeout --> callback --> throw
2、事件的错误处理
我们可以监听页面中任何 HTML 元素的事件,DOM 事件的错误处理机制遵循与任何异步 Web API 相同的方案。
来看下面的例子:
const button = document.querySelector("button");button.addEventListener("click", function() {throw Error("error");
});
这里,在单击按钮后立即抛出了异常,我们该如何捕获这个异常呢?这样写是不起作用的,也不会阻止程序崩溃:
const button = document.querySelector("button");try {button.addEventListener("click", function() {throw Error("error");});
} catch (error) {console.error(error.message);
}
与前面的 setTimeout 例子一样,任何传递给 addEventListener 的回调都是异步执行的:
Track A: --> try/catch
Track B: --> addEventListener --> callback --> throw
如果不想让程序崩溃,为了正确处理错误,就必须将 try/catch 放到 addEventListener 的回调中。不过这样做并不是最佳的处理方式,与 setTimeout 一样,异步代码路径抛出的异常无法从外部捕获,并且会使程序崩溃。
下面会介绍 Promises 和 async/await 是如何简化异步代码的错误处理的。
3、onerror
HTML 元素有许多事件处理程序,例如 onclick
、onmouseenter
、onchange
等。除此之外,还有 onerror
,每当 <img>
标签或 <script>
等 HTML 元素命中不存在的资源时,onerror 事件处理程序就会触发。
来看下面的例子:
<body><img src="nowhere-to-be-found.png">
</body>
当访问的资源缺失时,浏览器的控制台就会报错:
GET http://localhost:5000/nowhere-to-be-found.png
[HTTP/1.1 404 Not Found 3ms]
在 JavaScript 中,可以使用适当的事件处理程序“捕获”此错误:
const image = document.querySelector("img");image.onerror = function(event) {console.log(event);
};
或者使用 addEventListener 来监听 error 事件,当发生错误时进行处理:
const image = document.querySelector("img");image.addEventListener("error", function(event) {console.log(event);
});
此模式对于加载备用资源以代替丢失的图像或脚本很有用。不过需要记住:onerror 与 throw 或 try/catch 是无关的。
4、Promise 的错误处理
下面来通过最上面的 toUppercase 例子看看 Promise 是如何处理错误的:
function toUppercase(string) {if (typeof string !== "string") {throw TypeError("Expected string");}return string.toUpperCase();
}toUppercase(4);
对上面的代码进行修改,不返回简单的字符串或异常,而是分别使用 Promise.reject
和 Promise.resolve
来处理错误和成功:
function toUppercase(string) {if (typeof string !== "string") {return Promise.reject(TypeError("Expected string"));}const result = string.toUpperCase();return Promise.resolve(result);
}
从技术上讲,这段代码中没有任何异步的内容,但它可以很好地说明 Promise 的错误处理机制。
现在我们就可以在 then 中使用结果,并使用 catch 来处理被拒绝的 Promise:
toUppercase(99).then(result => result).catch(error => console.error(error.message));
输出结果如下:
在 Promise 中,catch 是用来处理错误的。除了 catch 还有 finally,类似于 try/catch 中的 finally。不管 Promise 结果如何,finally 都会执行:
toUppercase(99).then(result => result).catch(error => console.error(error.message)).finally(() => console.log("Finally"));
输出结果如下:
fileOf7174.png
需要记住,任何传递给 then/catch/finally 的回调都是由微任务队列异步处理的。 它们是微任务,优先于事件和计时器等宏任务。
5、Promise, error, throw
作为拒绝 Promise 时的最佳实践,可以传入 error 对象:
Promise.reject(TypeError("Expected string"));
这样,在整个代码库中保持错误处理的一致性。 其他团队成员总是可以访问 error.message,更重要的是可以检查堆栈跟踪。
除了 Promise.rejec
t 之外,还可以通过抛出异常来退出 Promise 执行链。来看下面的例子:
Promise.resolve("A string").then(value => {if (typeof value === "string") {throw TypeError("Expected number!");}
});
这里使用 字符串来 resolve 一个 Promise,然后执行链立即使用 throw 断开。为了停止异常的传播,可以使用 catch 来捕获错误:
Promise.resolve("A string").then(value => {if (typeof value === "string") {throw TypeError("Expected number!");}}).catch(reason => console.log(reason.message));
这种模式在 fetch 中很常见,可以通过检查 response 对象来查找错误:
fetch("https://example-dev/api/").then(response => {if (!response.ok) {throw Error(response.statusText);}return response.json();}).then(json => console.log(json));
这里的异常可以使用 catch 来拦截。 如果失败了,并且没有拦截它,异常就会在堆栈中向上冒泡。这本身并没有什么问题,但不同的环境对未捕获的拒绝有不同的反应。
例如,Node.js 会让任何未处理 Promise 拒绝的程序崩溃:
DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
所以,最好去捕获错误。
6、使用 Promise 处理定时器错误
对于计时器或事件,不能捕获回调抛出的异常。上面有一个例子:
function failAfterOneSecond() {setTimeout(() => {throw Error("Error");}, 1000);
}// 不生效
try {failAfterOneSecond();
} catch (error) {console.error(error.message);
}
我们可以使用 Promise 来包装计时器:
function failAfterOneSecond() {return new Promise((_, reject) => {setTimeout(() => {reject(Error("Error"));}, 1000);});
}
这里通过 reject 捕获了一个 Promise 拒绝,它带有一个 error 对象。此时就可以用 catch 来处理异常了:
failAfterOneSecond().catch(reason => console.error(reason.message));
这里使用 value 作为 Promise 的返回值,使用 reason 作为拒绝的返回对象。