在前端开发中,常见的异常主要可以分为几大类,这些异常通常是由于代码错误、浏览器环境、用户输入不当或网络问题引起的。
下面列举一些常见的十大异常及其原因。
一、类型错误(TypeError)
TypeError
是 JavaScript 中一个非常常见的异常类型,它发生在一个值的类型不符合预期时。这种错误通常意味着代码尝试执行不适用于该数据类型的操作。
当一个操作或函数遇到与其预期的数据类型不符的值时,就会抛出类型错误。常见的情形包括:
-
尝试调用非函数类型的变量。
-
访问null或undefined类型的属性。
-
传递错误类型的参数给函数。
下面是一些常见的 TypeError
场景及其示例:
-
访问 null 或 undefined 的属性或方法
在 JavaScript 中,null
和 undefined
是没有属性或方法的,尝试访问它们的属性或方法会导致 TypeError
。
重点检查Object类型的数据,比如Array或者普通对象,在调用相关方法时,注意做check。
如:Array的方法forEach、map等,对象的结构和链式调用等。
let obj = null;
console.log(obj.someProperty); // TypeError: Cannot read properties of null (reading 'someProperty')
let undefinedVar;
console.log(undefinedVar.someProperty); // TypeError: Cannot read properties of undefined (reading 'someProperty')
-
尝试调用非函数类型的值
当你尝试调用一个并非函数的值时,JavaScript 会抛出 TypeError
。
let num = 123;
num(); // TypeError: num is not a function
3. 修改只读属性
尝试修改一个对象的只读属性(如内置对象的某些属性)也会抛出 TypeError
。
const obj = {};
Object.defineProperty(obj, 'readOnly', {
value: 42,
writable: false
});
obj.readOnly = 100; // TypeError: Cannot assign to read only property 'readOnly' of object
4. 对象不支持属性或方法
如果你尝试在一个对象上调用它不支持的方法,会发生 TypeError
。
let num = 123;
console.log(num.toUpperCase()); // TypeError: num.toUpperCase is not a function
5. 传递错误类型的参数给函数
如果一个函数期望特定类型的参数,而传递了错误类型的参数,也可能会抛出 TypeError
。
function repeatString(s, times) {
if (typeof times !== "number") {
throw new TypeError("times must be a number");
}
return s.repeat(times);
}
console.log(repeatString("hello", "3")); // TypeError: times must be a number
6. 使用 new 关键字调用非构造函数
尝试使用 new
关键字调用非构造函数会导致 TypeError
,因为 new
是用来创建对象实例的。
const fn = () => {
console.log("This is not a constructor");
};
new fn(); // TypeError: fn is not a constructor
7. Symbol 类型作为构造函数
Symbol
是一个原始数据类型,不能作为构造函数使用。
let sym = new Symbol(); // TypeError: Symbol is not a constructor
二、异步代码错误
在使用Promise或async/await时,错误的处理不当可能导致异常被抛出。
-
忘记在Promise链中使用catch。
-
async函数中未处理的异常。
示例:
可以使用async/await + try/catch,Promise也可以用其自己的链式catch来捕获其异常。
注意关于Promise如何处理异常以及常见的方式和坑,参考下方。
async function fetchData() {
let response = await fetch('https://api.example.com/data');
let data = await response.json();
return data;
}
fetchData().catch(error => console.error('Error:', error));
三、网络请求异常
当进行AJAX请求或使用Fetch API时,网络问题或服务器问题可能导致请求失败。也属于异步代码错误。这里单独拎出来,因为比较重要和独立。
-
XMLHttpRequest的onerror事件。
-
Fetch API的catch处理。
示例:
fetch('https://api.example.com/data')
.then(response => response.json())
.catch(error => console.error('Fetch error:', error));
四、引用错误(ReferenceError)
在 JavaScript 中,ReferenceError
是在尝试引用一个未定义的变量或未能正确解析引用时抛出的错误。这类错误通常是由于变量未被声明(或未在当前作用域内声明),或者尝试访问的代码块无法正确执行导致的。当尝试引用一个未定义的变量时,会抛出引用错误。
示例:
console.log(x); // ReferenceError: x is not defined
五、语法错误(SyntaxError)
当代码中存在语法错误时,解析代码的过程中会抛出语法错误。这类错误通常在开发过程中就被发现并修正。
示例:
let x = ; // SyntaxError: Unexpected token ';'
let regex = /[/; // SyntaxError: unterminated character class
let number = 100.; // SyntaxError: missing digits after .
六、范围错误(RangeError)
当一个值不在其允许的范围或集合内时,就会抛出范围错误。
示例:
let num = 1;
console.log(num.toFixed(-1)); // RangeError: toFixed() digits argument must be between 0 and 100
七、URI错误(URIError)
当全局URI处理函数被传递一个不合法的URI时,会抛出URI错误。
示例:
decodeURIComponent('%'); // URIError: URI malformed
八、静态资源加载错误
当HTML文档中的外部资源如图片、脚本、样式表加载失败时,也会触发错误。
全局:
使用window.addEventListener("error")
我们可以通过捕获的方式全局监控加载失败的错误,虽然这也监控到了脚本错误,但通过 !(event instanceof ErrorEvent) 判断便可以筛选出加载失败的错误
window.addEventListener('error', (error) => {
console.log('捕获到异常:', error);
}, true)
局部:
资源标签onerror 属性来捕获
<img>
的onerror事件。<script>
的onerror事件。
function errorHandler(error) {
console.log("捕获到静态资源加载异常", error);
}
<script src="http://**/js/test.js" onerror="errorHandler(this)"></script>
<link
rel="stylesheet"
href="http://***/test.css"
onerror="errorHandler(this)"
/>
<img
src="invalid_link"
onerror="this.onerror=null;this.src='https://www.test-img.com/img/default-avatar-png.png';"
/>
这样可以拿到静态资源的错误,但缺点很明显,代码的侵入性太强了,每一个静态资源标签都要加上 onerror 方法。
是否也可以通过 window.onerror 去全局监听加载失败呢?答案是否定的。因为 onerror 的事件并不会向上冒泡,window.onerror 接收不到加载失败的错误。只能通过该资源标签的onerror 方法,才可以。
冒泡不行,但是捕获阶段可以,也即上面的addEventListener("error").
九、页面崩溃异常
window.addEventListener('load',()=>{
sessionStorage.setTitem('page_exit','pending')
})
window.addEventListener('beforeunload',()=>{
sessionStorage.setTitem('page_exit','true')
})
sessionStorage.getTitem('page_exit')!='true' // 页面崩溃
十、iframe异常
分三种情况:
-
iframe 页面和你的主站是同域名:直接给 iframe 添加 onerror 事件即可。
-
iframe 页面和你的主站不是同个域名,但是自己可以控制,可以通过与 iframe 通信的方式将异常信息抛给主站接收。与 iframe 通信的方式有很多,常用的如:postMessage,hash 或者 name 字段跨域等
-
iframe 页面和你的主站不是同个域名,且网站不受自己控制为第三方的话,没办法捕获,这是出于安全性的考虑,只能通过控制台看到详细的错误信息
前端常见的适合try catch的高危异常代码
在JavaScript开发中,合理使用try...catch
可以帮助你控制程序流程,处理运行时错误,避免因为一个未被捕获的异常而导致整个程序崩溃。这种异常处理机制特别适用于那些你预知可能会失败的操作,比如解析用户输入、处理外部数据、执行有风险的函数等。下面列举一些常见的可能抛出异常的代码场景,并提供具体的代码示例。
1. 解析JSON数据
当使用JSON.parse()
方法解析JSON字符串时,如果输入的字符串不是有效的JSON格式,则会抛出SyntaxError
。
代码示例
try {
const jsonString = '{"name":"John", "age":30}'; // 正确的JSON字符串
const user = JSON.parse(jsonString);
console.log(user);
} catch (error) {
console.error('JSON parse error:', error);
}
2. 正则表达式处理
- 使用正则表达式时,错误的模式或方法可能导致异常。
源码示例
try {
let regex = /(\d+)/g;
let matches = '123abc'.match(regex);
console.log(matches);
} catch (error) {
console.error('Regex error:', error);
}
-
访问未定义的对象属性
尝试访问一个未定义或未被初始化的对象的属性时,JavaScript会抛出TypeError
。
代码示例
// 方法1
const name = user?.name ?? 'default_name'
// 方法2
try {
let user;
console.log(user.name); // user未定义,尝试访问name属性会抛出TypeError
} catch (error) {
console.error('Error:', error);
}
3. 使用本地存储API
当使用Web的本地存储API(如localStorage)时,如果用户的浏览器禁用了本地存储,或者存储空间已满,尝试访问这些API可能会抛出异常。
代码示例
try {
localStorage.setItem('key', 'value'); // 尝试写入localStorage
} catch (error) {
console.error('Local Storage error:', error);
}
4. 处理DOM操作
在进行DOM操作时,如果操作的元素不存在,可能会导致错误。
代码示例
try {
const element = document.getElementById('nonexistentElement');
element.innerText = 'Hello'; // 如果元素不存在,这里会抛出TypeError
} catch (error) {
console.error('DOM operation error:', error);
}
5. 使用第三方库或API
当使用第三方库或调用外部API时,这些操作可能会失败,比如网络问题或数据格式问题。
代码示例
try {
// 假设someThirdPartyFunction是一个可能抛出异常的第三方函数
someThirdPartyFunction();
} catch (error) {
console.error('Third-party library error:', error);
}
常见的一些异常捕获方式及坑点
-
try-catch可疑代码块
注意:
能被 try catch 捕捉到的异常,必须是在报错的时候,线程执行已经进入 try catch 代码块,且处在 try catch 里面,这个时候才能被捕捉到。 如果是在之前,或者之后,都无法捕捉异常。
try catch能捕获捉到运行时非异步错误,无法捕获语法错误和异步错误。
所以try catch能捕获捉到运行时非异步错误,无法捕获语法错误和异步错误。适用于命令式代码,不适用于声明式如React等(React捕获异常后续会说明)。
try catch 不能捕获的js异常:
- 语法错误
- 普通异步任务如setTimeout
- Promise任务
- async任务需要await才能捕获
合理使用,不要过度使用。有得异常需要抛出去给外部处理。
常见的需要注意用try-catch包裹,捕获异常的情况
例子:
try { try_statements } [catch (exception) { catch_statements }] [finally { finally_statements }]
- JSON处理必须使用try catch捕获异常
try { const res=fetch(*) JSON.parse(res); // res 为服务端返回的数据 } catch(e) { // 捕获到详细的错误,在这里处理日志上报或其他异常处理等逻辑,如是否提示用户,是否有异常下的兜底数据,比如使用缓存数据等 console.error("服务端数据格式返回异常,无法解析", res); } // 注意:下面的异常try catch无法捕获 try { setTimeout(() => { undefined.map(v => v); }, 1000) } catch(e) { console.log("捕获到异常:", e); }
- async await异步请求
- 正则表达式处理
- buffer处理
-
Promise异常处理
注意:Promise 中的异常不能被 try-catch 和 window.onerror 捕获!(易错点!)
原因是,Promise 在执行回调中都用 try catch 包裹起来了,其中所有的异常都被内部捕获到了,并未往上抛异常。
局部Promise捕获两种方式:
// 1. Promise().catch() let promise = new Promise((resolve,reject)=>{}).catch(e=>{ // handle error }) // 2. async/await + try/catch let promise = new Promise(); async function test() { try { await promise; } catch (e) { // handle error } }
全局Promise异常用window.addEventListener("unhandledrejection")
注意:
- Promise自己的异常只能被自己catch, 或在try/catch里以await的方式调用来捕获。否则就会作为ERR_UNHANDLED_REJECTION异常抛出到全局。
- 外层Promise不能不能捕获内层Promise的异常。
let p1 = new Promise(async (resolve, reject) => {
return reject(100); // 被捕获
});
async function fn() {
try {
let result = await p1;
console.log(2, result); //这里不会执行
} catch (e) {
console.log("e:", e); //这里不会执行
}
}
fn();
let p1 = new Promise(async (resolve, reject) => {
return reject(100); // 未被捕获,会抛出全局异常:ERR_UNHANDLED_REJECTION
});
function fn() {
try {
let result = p1;
console.log(2, result); //这里不会执行
} catch (e) {
console.log("e:", e); //这里不会执行
}
}
fn();
let p1 = new Promise(async (resolve, reject) => {
console.log("after reject");
return Promise.reject(100); // 未被捕获,会抛出全局异常:ERR_UNHANDLED_REJECTION
});
async function fn() {
try {
let result = await p1;
console.log(2, result); //这里不会执行
} catch (e) {
console.log("e:", e); //这里不会执行
}
}
fn();
let p1 = new Promise(async (resolve, reject) => {
return Promise.reject(100); // 未被捕获,会抛出全局异常:ERR_UNHANDLED_REJECTION
}).catch((e) => {
console.log("promise out e", e); // 这里不会执行
});
async function fn() {
try {
let result = await p1;
console.log(2, result); //这里不会执行
} catch (e) {
console.log("e:", e); //这里不会执行
}
}
fn();
- Promise里同步抛出的异常,会触发Promise.reject而被捕获。但异步抛出的异常,不会触发Promise.reject,因此不会被捕获。
new Promise((resolve, reject) => {
throw new Error("Error1"); // 等效于reject
}).catch((e) => {
console.log("异常被捕获到了1");
});
new Promise(async (resolve, reject) => {
reject(new Error("Error2"));
}).catch((e) => {
console.log("异常被捕获到了2");
});
new Promise(async () => {
throw new Error("Error3");
}).catch((e) => {
console.log("异常被捕获到了3");
});
注意未被捕获的promise异常会作为全局异常抛出。
为了防止有漏掉的 Promise
异常,建议在全局增加一个对 unhandledrejection
的兜底监听,用来全局监听Uncaught Promise Error
。
window.addEventListener("unhandledrejection", function(e){
console.log(e);
// 异常上报等异常处理
});
-
默认赋值
对象的处理,obj需要对null和undefined进行判断处理,否则会报错。
注意:默认赋值语句,null不会执行默认赋值,undefined会执行默认赋值
export const testObjFn1 = (obj = { a: 1 }) => {
return obj.a;
};
console.log(testObjFn1()); // 1
console.log(testObjFn1(undefined)); // 1
console.log(testObjFn1(null)); // TypeError: Cannot read properties of null (reading 'a')