JS错误捕获

172 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第10天,点击查看活动详情

try {

前端对错误捕获的正确应用是保证代码健壮性的关键 😉。

那么,如何来捕获一只错误(error)呢 🧐。

首先,你需要识别它的种类(同步/异步) 😜,

其次,根据它的种类,检查自己背包中相应的工具(try…catch… /Promise.catch) 🤠。

让我们来看一下实际的场景吧~

} catch (error) {

alert(”欢迎留言探讨!”)

}

使用 try…catch… 捕获同步错误

补充说明一下,这里的同步或者异步错误指的是错误发生的那段代码执行是同步执行的还是异步执行的。

我们先来看最简单的情况,也就是同步错误的捕获。下面这个例子来自MDN。

try...catch - JavaScript | MDN

try {
  try {
    throw new Error("oops");
  }
  catch (ex) {
    console.error("inner", ex.message);
    throw ex;
  }
  finally {
    console.log("finally");
  }
}
catch (ex) {
  console.error("outer", ex.message);
}

// Output:
// "inner" "oops"
// "finally"
// "outer" "oops"

这里使用嵌套的try…catch…进行错误捕获,顺便帮助我们复习了

  1. try…catch…finally 的触发顺序
  2. 嵌套try…catch…是如何进行错误捕获的

上面的这个过程中均为同步的代码,所以正确使用try…catch…就可以实现错误捕获了。需要注意的点是

  1. catch 作为一个子句,有一个标识错误的参数,这个参数仅在catch语句块中有效。
  2. 在 catch 子句中是可以 return 的,有些同学可能误解catch作为一个异常链路的语句,是不是很少用(大多是控制台打印错误 😅),不是的,恰好相反,很多时候需要在 catch 中添加一些兜底逻辑,保证程序的正常运行,又或者是将错误反馈给用户。

使用 try…catch…捕获异步错误

有些异步函数,表面看起来却是同步执行的样子,相信大家立刻就能想到 async…await…了吧。

的确,使用try…catch…捕获这类async函数内部的错误也是蛮好用的。

首先简单复习一下 async function~

async function 执行后会返回一个promise,因此其不同的return值会决定这个promise的状态(resolved/rejected)。

  • resolved

    • 无返回值: 返回一个 resolved 状态的promise, promise中的值为 undefined。
    async function foo() {
       await 1
    }
    // equals to
     
    async function bar() {
     return Promise.resolve(1).then(() => undefined)
    }
    
    • return 非promise类型:返回一个 resolved状态的promise,promise中的值为返回值。
    async function foo() {
     return true
    }
    // equals to 
    async function bar() {
     return Promise.resolve(true)
    }
    
    • return promise:假设返回的这个promise是resolved状态,那么最终返回的就是该promise。
    async function foo() {
     return new Promise((reslove, reject) => {
      setTimeout(() =>{
       resolve("resolved string")
      }, 2000)
     })
    }
    
    // equals to 
    
    async function bar() {
     return await new Promise((reslove, reject) => {
      setTimeout(() =>{
       resolve("resolved string")
      }, 2000)
     })
    }
    

    注:上面这个例子仅在返回 resolved 状态下的promise的情况下才是等价的两个函数,如果是rejected状态,会在下文介绍。

  • rejected

    • 执行过程中出错并且无错误处理

      这里的出错可以是同步错误,也可以是在 await 后的异步语句执行出错并且没有捕获。其表现是可以自动中断执行并将错误使用隐式promise(rejected)传递错误给调用者

    • 显式返回rejected状态的promise,我们来看一下下面这两种写法:

      async function foo() {
       return new Promise((reslove, reject) => {
        setTimeout(() =>{
         reject("reject string")
        }, 2000)
       })
      }
      
      // equals to 
      
      async function bar() {
       return await new Promise((reslove, reject) => {
        setTimeout(() =>{
         resolve("reject string")
        }, 2000)
       })
      }
      

      上面的这个例子中,虽然返回的都是错误值为“reject string”且状态为rejected的promise,但是有一些细微差别,foo返回的是原来的promise对象,而bar在return之前就抛出了错误,该错误被包装成一个新的rejected状态的promise对象。

熟悉了 async function 的返回值后,如果是使用 try…catch…进行错误处理,很明显利用了await的特性(异步代码使用同步的方式执行),是能够捕获await语句的错误的。


// 可以捕获 returnRejectedPromise 的错误
async function foo() {
 try{
  return await returnRejectedPromise()
 }catch(err) {
  // 错误处理
 }
}

// 不能捕获 returnRejectedPromise 的错误
async function bar() {
 try{
  return returnRejectedPromise()
 }catch(err) {
  // 错误处理
 }
}

那么,当错误发生的时候async函数返回值有什么变化呢,其实,这取决于后续代码的逻辑了,既可以catch中提前返回也可以在catch包裹的代码块之外返回,和刚刚列举的两种状态对应的不同返回值的情况是一样的,最终async 函数还是会返回一个可以链式调用的promise。

下面这个例子来自这篇博客,展示了使用try…catch…处理async function错误时的执行顺序。

Error handling with Async/Await in JS

async function thisThrows() {
    throw new Error("Thrown from thisThrows()");
}

async function myFunctionThatCatches() {
    try {
        return await thisThrows(); // <-- Notice we added here the "await" keyword.
    } catch (e) {
        console.error(e);
    } finally {
        console.log('We do cleanup here');
    }
    return "Nothing found";
}

async function run() {
    const myValue = await myFunctionThatCatches();
    console.log(myValue);
}

run();

// Outptut:
// Error: Thrown from thisThrows()
//   ...stacktrace
// We do cleanup here
// Nothing found

使用 Promise.catch捕获异步错误

还是首先简单review一下promise的catch机制吧~

// 抛出一个错误,大多数时候将调用 catch 方法
var p1 = new Promise(function(resolve, reject) {
  throw 'Uh-oh!';
});

p1.catch(function(e) {
  console.log(e); // "Uh-oh!"
});

// 在异步函数中抛出的错误不会被 catch 捕获到
var p2 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    throw 'Uncaught Exception!';
  }, 1000);
});

p2.catch(function(e) {
  console.log(e); // 不会执行
});

// 在 resolve() 后面抛出的错误会被忽略
var p3 = new Promise(function(resolve, reject) {
  resolve();
  throw 'Silenced Exception!';
});

p3.catch(function(e) {
   console.log(e); // 不会执行
});

使用 Promise.catch() 捕获

  1. 异步函数(setTimeout)中抛出的错误不会被捕获。
  2. 在resolve() 后面抛出的错误会被忽略。
  3. catch 方法接收的是 rejected 状态的 promise,返回值自定义。如果没有显式的 return 语句,返回的是值为 undefined 的 resolved 状态的 promise,后续是可以继续链式调用 then 方法的。当然,也可以自行调用 return 返回有值的 resolved 状态的 promise,或者再次抛出错误。
  4. 错误冒泡,链式调用中的错误最终会被距离最近的 Promise.catch 方法捕获。而类似的,try…catch…也有嵌套的就近原则,即错误会被距离最近的catch语句捕获,而未捕获的错误会一直向上层冒泡,被捕获或者在全局抛出。

结合上文,我们比较一下在使用 async function 时可以使用的错误捕获方式 😀。

  • 使用 try…catch…的方式

    async function getProcessedData(url) {
      let v;
      try {
        v = await downloadData(url);
      } catch (e) {
        v = await downloadFallbackData(url);
      }
      return processDataInWorker(v);
    }
    
  • 使用 Promise.catch()的方式

    function getProcessedData(url) {
      return downloadData(url) // 返回一个 promise 对象
        .catch(e => {
          return downloadFallbackData(url)  // 返回一个 promise 对象
        })
        .then(v => {
          return processDataInWorker(v); // 返回一个 promise 对象
        });
    }
    

上面的这两个例子足够简洁,也充分说明了这两种方式的处理特点。当然,我们在实际开发过程中,经常会遇到一些更复杂的场景,更多的异步操作,如何正确进行错误处理,避免过多的嵌套或者过长的调用链,之后再继续探讨吧 🤩~