在开发过程中,我们的目标是 0error,0warning
。
但有很多因素并不是我们可控的,为了避免某块代码的错误,影响到其他模块或者整体代码的运行,我们经常会使用try-catch
模块来主动捕获一些异常或者错误。
比如我们在获取 url
中的参数后,对其进行 JSON
解析,这里就要用try-catch
包裹一下,因为我们不能保证获取到的参数一定是可以正常解析的:
const user= getQueryString('user');
if (user) {
try {
const { id, username} = JSON.parse(user);
console.log(id, username);
} catch (err) {
console.error(err);
}
}
用户在复制链接的过程中,有可能会有意无意地复制不完全,导致整个参数不完整,JSON.parse
无法解析不完整的 json string
。为了避免因数据不完整造成的 JSON
解析错误,我们可以将其用try-catch
包括起来。
我们经常会使用try-catch
模块来主动捕获一些异常或者错误,避免此块的代码影响到其他模块或者整体代码的运行。但有些情况,try-catch
并不能捕获到代码中的异常!
当我们使用 xhr 请求接口,若接口不支持跨域时,浏览器会在控制台提示错误:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>trycatch演示</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.js"></script>
</head>
<body>
<button onclick="cli()">点我</button>
<script>
function cli() {
try {
$.get('http://www.baidu.com', function success(data) {
console.log(data)
})
} catch (e) {
console.log(e)
}
}
</script>
</body>
</html>
Access to XMLHttpRequest at 'https://www.baidu.com/' (redirected from 'http://www.baidu.com/') from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
通过图片中我们可以看到,请求接口时产生了跨域错误,但并没有进入到catch中。我们通过 xhr.onerror 的监听,是可以知道 xhr 请求产生了请求错误的。
若 try
中异步的模块产生了错误,catch
也是捕获不到的,例如:
// setTimeout中的错误
try {
setTimeout(function () {
throw new Error('error in setTimeout'); // 200ms后会把异常抛出到全局
}, 200);
} catch (err) {
console.error('catch error', err); // 不会执行
}
// Promise中的错误
try {
Promise.resolve().then(() => {
throw new Error('error in Promise.then');
});
} catch (err) {
console.error('catch error', err);
从运行结果可以看到,这两种代码,均没有进入到 catch
模块中。
为什么没有进入到catch模块中呢?
当异步函数抛出异常时,对于宏任务而言,执行函数时已经将该函数推入栈,此时并不在 try-catch 所在的栈,所以 try-catch 并不能捕获到错误。对于微任务而言,比如 promise,promise 的构造函数的异常只能被自带的 reject 也就是.catch 函数捕获到。
捕获异步的错误
答案就是把 try-catch
放到异步代码的里面。
1.将try-catch放到setTimeout内部
// 将try-catch放到setTimeout内部
setTimeout(() => {
try {
throw new Error('error in setTimeout');
} catch (err) {
console.error('catch error', err);
}
}, 200);
// 将try-catch放到then内部
Promise.resolve().then(() => {
try {
throw new Error('error in Promise.then');
} catch (err) {
console.error('catch error', err);
}
});
3.使用Promse自带的catch捕获异常
// 使用Promse自带的catch捕获异常
Promise.resolve()
.then(() => {
throw new Error('error in Promise.then');
})
.catch((err) => {
console.error('Promise.catch error', err);
});
Promise
有一个很大的优势是,它自带着异常捕获方法catch()
,在 then()
方法产生错误导致代码无法运行时,会自动进入到 catch()
方法中。因此建议写 Promise
时,都把 catch()
写上,否则未捕获的异常,就会冒泡到全局。
await
可与.catch()
同时使用。try/catch
能捕获所有异常, .catch
只能捕获异步方法中reject
错误
也就是说,如果只想捕获 异步方法中reject
错误的话,在使用await
的同时使用.catch()
就能捕获到。但捕获不到reject之外的异常。
如下图:
1.同时使用捕获到了reject
异常:
const foo=()=>{
return new Promise((resolve, reject) => {
throw new Error('throw 错误')
});
}
await foo().catch(e=>{console.log('捕获到了:'+e)});
2.捕获不到reject
之外的异常,直接报错:
(这种情况就需要使用try/catch捕获处理了)
const foo=()=>{
throw new Error('throw 错误');
return new Promise((resolve, reject) => {
resolve('resolve 错误')
});
}
await foo().catch(e=>{console.log('捕获到了:'+e)});
但这种捕获异常后,外层的 catch()
方法就捕获不到异常了,不再继续向外层冒泡了。正确的做法是,底层模块产生的错误,应当直接抛出给业务层,让业务层决定这个错误怎么处理,而不是直接吞掉。
多层 try-catch
时,会被最内层的 catch()
方法捕获到,然后就不再向外层冒泡:
try {
try {
throw new Error('error');
} catch (err) {
console.error('内层的catch', err); // 内层的catch Error: error
}
} catch (err) {
console.error('最外层的catch', error);
}
在了解使用try-catch
之前,我们先来了解下 js
中有哪些个原生的错误类型。
js
代码在运行时可能产生的错误错误共有 6 种类型:
SyntaxError
);TypeError
);RangeError
);eval
错误(EvalError
);ReferenceError
);URI
错误(URIError
);这些错误类型都继承自Error
类。
SyntaxError
)语法错误,通常是开发者在开发过程,代码语句写的有问题,浏览器无法对其进行解析:
const a=;
错误信息:
VM202:1 Uncaught SyntaxError: Unexpected token ';'
TypeError
);类型错误通常会出现在两种情况:
concat
操作;null
或者 undefined
值:const obj = {};
obj.concat([1]);
错误信息
VM211:2 Uncaught TypeError: obj.concat is not a function
at <anonymous>:2:5
const a = null;
a.nickname; //VM218:2 Uncaught TypeError: Cannot read properties of null (reading 'nickname')at :2:3
在编写一些方法供给其他模块调用时,当在检查到参数传入为空或者 null
等空置时,可以抛出TypeError
的错误。
RangeError
);该错误通常是因为传入的参数,超出了规定的范围。例如toFixed()
方法可以接受 0-100
范围内的数值,当超过这个范围时,就会抛出该错误。
Math.PI.toFixed(105); // Uncaught RangeError: toFixed() digits argument must be between 0 and 100
eval
错误(EvalError
);这种错误一般很少会遇到,因为使用 eval
操作时,即使不正当的错误,也会抛出其他类型的错误。
new eval(); // Uncaught TypeError: eval is not a constructor
eval = 1234; // 正确执行
ReferenceError
);引用错误表示师徒访问一个未经声明的变量:
console.log(nick); // Uncaught ReferenceError: nick is not defined
URI
错误(URIError
);该错误通常是一些操作 uri 函数抛出的错误,主要包括:encodeURI(), decodeURI(), encodeURIComponent(), decodeURIComponent(), escape(), unescape()
。
decodeURIComponent('%'); // Uncaught URIError: URI malformed
decodeURIComponent('%23'); // # 正确执行
对于稍微大点的模块,我们想自定义一些错误类型,通过这些错误类型,就能看出是某个模块抛出的错误。该怎么写呢?
我们自定义的错误类型也是要继承自Error
类的,实现起来非常简单:
class FingerError extends Error {
constructor(message) {
super(message);
this.name = 'FingerError'; // 该错误的名称
Error.captureStackTrace(this, this.constructor); // 获取错误堆栈的信息
}
}
const err = new FingerError('get name error');
console.error(err); // FingerError: get name error
err instanceof FingerError; // true
前端中还有很多种产生错误的方式的,我们平时就要注意避免这些错误。我们接下来也可以从错误监控的角度来分析下,如何来监控页面中出现的错误和错误类型。