跳到主要内容

认识

2024年05月08日
柏拉文
越努力,越幸运

一、代码执行异常


1.1 错误类型

  • Error: 最基本的错误类型,其他的错误类型都继承自该类型。通过 Error,我们可以自定义 Error 类型。

  • RangeError: 范围错误。当出现堆栈溢出(递归没有终止条件)、数值超出范围(new Array 传入负数或者一个特别大的整数)情况时会抛出这个异常。

  • ReferenceError: 引用错误。当一个不存在的对象被引用时发生的异常。

  • SyntaxError: 语法错误。如变量以数字开头;花括号没有闭合等。

  • TypeError: 类型错误。如把 numberstr 使用。

  • URIError: 向全局 URI 处理函数传递一个不合法的 URI 时,就会抛出这个异常。如使用 decodeURI('%')decodeURIComponent('%')

  • EvalError: 一个关于 eval 的异常,不会被 javascript 抛出。

1.2 try catch

try{}catch(){} 能捕捉到的异常必须是线程执行已经进入 try catchtry catch 未执行完的时候抛出来的。当程序运行到 try catch 里面时,如果未报错,则忽略 catch 中的代码,若报错,则不执行 try 报错内容后面的代码,转而执行 catch 中的代码。try{}catch(){} 有如下特点:

  1. 无法捕获语法错误: 因为语法错误是在语法检查阶段就报错了,线程执行尚未进入 try catch 代码块,自然就无法捕获到异常。

  2. 无法捕获异步错误: 因为等异步函数里面的事件进入事件队列的时候,主线程已经离开了try catch,所以try catch是无法捕获异步函数的错误的。

  3. 无法捕获 Promise 异常: 当异步函数抛出异常时,对于宏任务而言,执行函数时已经将该函数推入栈,此时并不在 try-catch 所在的栈,所以 try-catch 并不能捕获到错误。

语法

// 示例1:常规运行时错误,可以捕获 ✅
try {
let a = undefined;
if (a.length) {
console.log('111');
}
} catch (e) {
console.log('捕获到异常:', e);
}

// 示例2:语法错误,不能捕获 ❌
try {
const notdefined,
} catch(e) {
console.log('捕获不到异常:', 'Uncaught SyntaxError');
}

// 示例3:异步错误,不能捕获 ❌
try {
setTimeout(() => {
console.log(notdefined);
}, 0)
} catch(e) {
console.log('捕获不到异常:', 'Uncaught ReferenceError');
}

1.3 window.onerror

window.onerror 事件在冒泡阶段执行, 用于全局捕获错误。 window.onerror 得到了五个参数,获取的报错堆栈相对很完整。 window.onerror 可以捕获异步错误; 捕获不到静态资源加载异常的错误,原因是资源类型错误没有冒泡,只能在捕获阶段捕获,而 window.onerror 是通过在冒泡阶段捕获错误,对静态资源加载类型异常无效,所以只能借助 window.addEventListener('error', callback, true) 的方式捕获。

语法

window.onerror = function(message, source, lineno, colno, error) { ... }
  • message:错误信息(字符串)。可用于HTML onerror=处理程序中的event

  • source:发生错误的脚本URL(字符串)

  • lineno:发生错误的行号(数字)

  • colno:发生错误的列号(数字)

  • errorError 对象

示例

window.onerror = function(message, source, lineno, colno, error) {
console.log("捕获到的错误信息是:", message, source, lineno, colno, error);
};

// 示例1:常规运行时错误,可以捕获 ✅
console.log(notdefined);

// 示例2:语法错误,不能捕获 ❌
const notdefined;

// 示例3:异步错误,可以捕获 ✅
setTimeout(() => {
console.log(notdefined);
}, 0);

// 示例4:资源错误,不能捕获 ❌
let script = document.createElement("script");
script.type = "text/javascript";
script.src = "https://www.test.com/index.js";
document.body.appendChild(script);

1.4 window.addEventListener("error")

window.addEventListener('error', true) 用于在捕获阶段全局捕获错误。可以捕获异步错误、可以捕获网络异常、静态资源加载异常错误。但是通过 window.addEventListener('error', true) 获取的报错堆栈不是很完整。

window.addEventListener("error", function (event) {
const { error } = event;
console.log("error: ", error.message);
console.log("error: ", error.stack);
});

二、Promise 异常


2.1 promise().catch()

Promise.reject(); // Uncaught (in promise) undefined

2.2 window.onrejectionhandled

Promisereject并且错误信息已经被catch处理的时候,这个错误不会被window.onerror以及window.addEventListener("error",handler,true)捕获。但是有专门的rejectionhandled事件方法进行捕获处理

语法

Promise.reject(); // Uncaught (in promise) undefined

2.3 window.onunhandledrejection

Promisereject并且错误信息没有被catch处理的时候,这个错误不会被window.onerror以及window.addEventListener("error",handler,true)捕获。但是有专门的unhandledrejection事件方法进行捕获处理

语法

Promise.reject(); // Uncaught (in promise) undefined

2.4 window.addEventListener("rejectionhandled")

Promisereject并且错误信息已经被catch处理的时候,这个错误不会被window.onerror以及window.addEventListener("error",handler,true)捕获。但是有专门的rejectionhandled事件方法进行捕获处理

语法

window.addEventListener("rejectionhandled", event => {
console.log("Promise rejected; reason: " + event.reason);
}, false);

window.onrejectionhandled = ()=>{

}

Promise.reject(throw new Error("xxx")).catch(error=> { console.log(error) });

2.5 window.addEventListener("unhandledrejection")

Promisereject并且错误信息没有被catch处理的时候,这个错误不会被window.onerror以及window.addEventListener("error",handler,true)捕获。但是有专门的unhandledrejection事件方法进行捕获处理

语法

window.addEventListener("unhandledrejection", event => {
console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
});

window.onunhandledrejection = event => {
console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
};

Promise.reject(throw new Error("xxx"));

三、网络请求异常


在浏览器端发起一个接口请求时,如果请求的 url 的有问题,也会抛出异常。

3.1 xhr.onerror

如果是 xhr.send 方法执行时出现异常,可以通过 xhr.onerror 或者 xhr.addEventListener('error', callback) 的方式捕获异常。

xhr.open('get''/user/userInfo');
xhr.send(); // send localhost:3000/user/userinfo net::ERR_FAILED

3.2 xhr.addEventListener('error',callback)

如果是 xhr.send 方法执行时出现异常,可以通过 xhr.onerror 或者 xhr.addEventListener('error', callback) 的方式捕获异常。

xhr.open('get''/user/userInfo');
xhr.send(); // send localhost:3000/user/userinfo net::ERR_FAILED

3.3 window.onerror

如果是 xhr.open 方法执行时出现异常,可以通过 window.addEventListener('error', callback) 或者 window.onerror 的方式捕获异常。

xhr.open('GET', "https://")  // Uncaught DOMException: Failed to execute 'open' on 'XMLHttpRequest': Invalid URL
at ....

3.4 window.addEventListener('error', callback)

如果是 xhr.open 方法执行时出现异常,可以通过 window.addEventListener('error', callback) 或者 window.onerror 的方式捕获异常。

xhr.open('GET', "https://")  // Uncaught DOMException: Failed to execute 'open' on 'XMLHttpRequest': Invalid URL
at ....

3.5 fetch(url).then(callback).catch(callback)

接口调用是通过 fetch 发起的, 我们可以通过 fetch(url).then(callback).catch(callback) 的方式去捕获异常。

四、静态资源加载异常


有的时候,如果我们页面的imgjscss 等资源链接失效,就会提示资源类型加载如异常。

4.1 xxx.onerror

img.onerror = function(){};

link.onerror = function(){};

script.onerror = function(){};

4.2 window.addEventListener('error', callback, true)

针对这一类的异常,我们可以通过 window.addEventListener('error', callback, true) 的方式进行全局捕获。这里要注意一点,使用 window.onerror = callback 的方式是无法捕获静态资源类异常的。原因是资源类型错误没有冒泡,只能在捕获阶段捕获,而 window.onerror 是通过在冒泡阶段捕获错误,对静态资源加载类型异常无效,所以只能借助 window.addEventListener('error', callback, true) 的方式捕获。

<img src="localhost:3000/data.png" /> // Get localhost:3000/data.png net::ERR_FILE_NOT_FOUND

五、跨域脚本执行异常


当项目中引用的第三方脚本执行发生错误时,会抛出一类特殊的异常。这类型异常和我们刚才讲过的异常都不同,它的 msg 只有 Script error 信息,没有具体的行、列、类型信息。

之以会这样,是因为浏览器的安全机制: 浏览器只允许同域下的脚本捕获具体异常信息,跨域脚本中的异常,不会报告错误的细节,这样可以有效避免敏感信息无意中被第三方(不受控制的)脚本捕获到

5.1 window.addEventListener('error', callback)

针对这类型的异常,我们可以通过 window.addEventListener('error', callback) 或者 window.onerror 的方式捕获异常。当然,如果我们想获取这类异常的详情,需要做以下两个操作:

  1. 在发起请求的 script 标签上添加 crossorigin="anonymous"
<script src="http://xxxxxxxx/texterror.js" crossorigin="anonymous"></script>
  1. 请求响应头中添加 Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: *

这样就可以获取到跨域异常的细节信息了。

六、Vue 异常


Vue 项目中,vue 使用 try/catch 来捕获常规代码的报错,被捕获的错误会通过 console.error 输出而避免应用崩溃。 因此 window.onerrorerror 事件不能捕获到常规的代码错误。如下所示:

异常代码

export default {
created() {
let a = null;
if (a.length > 1) {
// ...
}
}
};

main.js 中捕获代码

window.addEventListener('error', (error) => {
console.log('error', error);
});
window.onerror = function (msg, url, line, col, error) {
console.log('onerror', msg, url, line, col, error);
};

控制台会报错,但是 window.onerrorerror 不能捕获到。

Vue 通过 Vue.config.errorHandler 或者 app.config.errorHandler 来捕获异常:

Vue.config.errorHandler = function (err, vm, info) {
// handle error
// `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子
// 只在 2.2.0+ 可用
}
app.config.errorHandler = (err, instance, info) => {
// 处理错误,例如:报告给一个服务
}

七、React 异常


React16 开始,官方提供了 ErrorBoundary 错误边界的功能,被该组件包裹的子组件,render 函数报错时会触发离当前组件最近父组件的 ErrorBoundary。生产环境,一旦被 ErrorBoundary 捕获的错误,也不会触发全局的 window.onerrorerror 事件

父组件代码

import React from 'react';
import Child from './Child.js';

// window.onerror 不能捕获render函数的错误 ❌
window.onerror = function (err, msg, c, l) {
console.log('err', err, msg);
};

// error 不能render函数的错误 ❌
window.addEventListener(
'error',
(error) => {
console.log('捕获到异常:', error);
},
true
);

class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}

static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// componentDidCatch 可以捕获render函数的错误
console.log(error, errorInfo);

// 同样可以将错误日志上报给服务器
reportError(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}

function Parent() {
return (
<div>
父组件
<ErrorBoundary>
<Child />
</ErrorBoundary>
</div>
);
}

export default Parent;

子组件代码

// 子组件 渲染出错
function Child() {
let list = {};
return (
<div>
子组件
{list.map((item, key) => (
<span key={key}>{item}</span>
))}
</div>
);
}
export default Child;

参考资料


使用 Sentry 做异常监控 - Sentry 是如何做到自动捕获前端应用异常的呢 ?