前端常见十大异常及如何处理

42 阅读11分钟

在前端开发中,常见的异常主要可以分为几大类,这些异常通常是由于代码错误、浏览器环境、用户输入不当或网络问题引起的。

下面列举一些常见的十大异常及其原因。

一、类型错误(TypeError)

TypeError 是 JavaScript 中一个非常常见的异常类型,它发生在一个值的类型不符合预期时。这种错误通常意味着代码尝试执行不适用于该数据类型的操作。

当一个操作或函数遇到与其预期的数据类型不符的值时,就会抛出类型错误。常见的情形包括:

  • 尝试调用非函数类型的变量。

  • 访问null或undefined类型的属性。

  • 传递错误类型的参数给函数。

下面是一些常见的 TypeError 场景及其示例:

  1.   访问 null 或 undefined 的属性或方法

  在 JavaScript 中,nullundefined 是没有属性或方法的,尝试访问它们的属性或方法会导致 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')
  1.   尝试调用非函数类型的值

  当你尝试调用一个并非函数的值时,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异常

分三种情况:

  1. iframe 页面和你的主站是同域名:直接给 iframe 添加 onerror 事件即可。

  2. iframe 页面和你的主站不是同个域名,但是自己可以控制,可以通过与 iframe 通信的方式将异常信息抛给主站接收。与 iframe 通信的方式有很多,常用的如:postMessage,hash 或者 name 字段跨域等

  3. 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);
}
  1. 访问未定义的对象属性

尝试访问一个未定义或未被初始化的对象的属性时,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);
}

常见的一些异常捕获方式及坑点

  1. 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
    }]
    
    1. 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);
    }
    
    1. async await异步请求
    2. 正则表达式处理
    3. buffer处理
  1. 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")

注意:

  1. Promise自己的异常只能被自己catch, 或在try/catch里以await的方式调用来捕获。否则就会作为ERR_UNHANDLED_REJECTION异常抛出到全局。
  2. 外层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();
  1. 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);
  // 异常上报等异常处理
});
  1. 默认赋值

对象的处理,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')