错误捕获,try catch能不能捕获到promise.reject()的错误?

11,429 阅读18分钟

本文主要是按照js高级程序第4版错误处理与调试的顺序进行书写

之前有这样的疑问,不是说try catch能捕获错误吗?对于promise.reject()的怎么不行?

既然不行,为啥await又与try catch一起用?

try catch在什么时候才会生效

啥时候用try catch捕获错误

结论:

1. try catch不能捕获异步代码,所以不能捕获promise.reject()的错误,并且promise期约故意将异步行为封装起来,从而隔离外部的同步代码
2. try catch 能对promise的reject()落定状态的结果进行捕获
3. try catch能捕捉到的异常,必须是`主线程执行`已经进入 try catch, 但 try catch 尚未执行完的时候抛出来的,
意思是如果将执行try catch分为前中后.只有中才能捕获到异常
4. 应该只在确切知道接下来该做什么的时候捕获错误(这里我单指try catch)
总结这篇,真的非常细,一步一步带着走,大概花了一周时间吧,所以真的值得一看,喜欢的话就收藏吧,我也会继续努力的!!!冲呀

一 为什么要捕获错误,处理错误

默认情况下,所有浏览器都会隐藏错误信息。一个原因是除了开发者之外这些信息对别人没什么用,另一个原因是网页在正常操作中报错的固有特性。

当网页中的 JavaScript 脚本发生错误时,不同浏览器的处理方式不同,而且也只会在浏览器输出, 有一个良好的错误处理策略可以让用户知道到底发生了什么, 防止用户流失。为此,必须理解各种捕获和处理 JavaScript错误的方式

二 错误一般出现在哪里

pc端: 所有现代桌面浏览器都会通过控制台暴露错,也就是pc端能够在控制台去捕获错误

移动端: 移动浏览器不会直接在设备上提供控制台界面。不过,还是有一些途径可以在移动设备中检查错 ---这里我推荐vconsole包

具体怎么用? 看我这篇文章: juejin.cn/post/692241… 为了防止你们这些臭屁的流失,我先上个图

三 错误捕获处理的方式try catch基本介绍

3.1.try catch的出现,处理异常的方法之一

任何可能出错的代码都应该放到 try 块中,而处理错误的代码则放在 catch 块中

try {
// 可能出错的代码
} catch (e) {
// 出错时要做什么
}

3.2 try 或 catch 块无法阻止 finally 块执行

3.2.1调用了window没有的方法, 必然报错,走catch,这时候finanlly有返回


   try {
      window.someNonexistentFunction();
      console.log('try')
    } catch(e) {
      console.log(e, 'catch')
    } finally {
      console.log('finally')
    }

3.2.2没有报错,这时候finanlly有返回

    try {
      console.log('try')
    } catch(e) {
      console.log(e, 'catch')
    } finally {
      console.log('finally')
    }

3.2.3那么对于finally我们能用他做啥呢?

1.例如loading的取消,不管我报错没, 最后这个loading必须是要取消的
2.比如做一些清除,销毁的操作

3.3 return 语句无法阻止finally的执行

遇到return一般是出栈,出去这个函数,但是这里的finally仍然会执行

   function testFinally(){
      try {
        return 2;
      } catch (error){
        return 1;
      } finally {
        return 0;
      }
    }
    console.log(testFinally());

四 try catch是如何捕获错误的?什么时候才能捕获到错误 (非常重要)

try catch能捕捉到的异常,必须是主线程执行已经进入 try catch, 但 try catch 尚未执行完的时候抛出来的,意思是如果将执行try catch分为前中后.只有中才能捕获到异常

4.1 try catch 之前 (捕获不到的)

发现了没? 连6666都没有打印,比如语法异常(syntaxError),因为语法异常是在语法检查阶段就报错了,线程执行尚未进入 try catch 代码块,自然就无法捕获到异常

4.2 try catch进行之中(能捕获到)

    try {
      window.someNonexistentFunction();
    } catch(e){
       console.log("error",e);
    }
    console.log('我要执行')

这时候就能捕获到了

4.3 try catch进行之中(重点:不能捕获到)

 try {
      console.log('try里面')
      setTimeout(() => {
        console.log('try里面的setTimeOut')
        window.someNonexistentFunction(); // 变成了未捕获的错误
      }, 0)
    } catch (e) {
      console.log('error', e)
    }
    setTimeout(() => {
      console.log('我要执行')
    }, 100)

这里没有catch到错误,是因为try catch是同步代码(一般我们用setTimeOut来模拟异步,也是因为他有延迟的特性,所以我们这里视为有异步操作)

当js运行到try里面setTimeOut的时候,setTimeOut先是去了setTimeOut线程,0秒后去task queue(任务队列)进行排队,运行到try catch外面的setTimeOut, 第二个setTimeOut也是先去setTimeOut线程,100/1000秒然后去task queue排队,排在第一个后面

那么执行的顺序就是try catch同步代码在主执行栈执行完了之后,去task queue看看有啥要执行的没,task queue里面是遵循先进先出的原则,自然按照图片上的顺序进行

这个例子也是说明了为啥try catch不能捕获到promise.resolve报错的一个原因

这里涉及到eventloop,下次补充

4.4 Promise的异常捕获

如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。

捕获Promise内部错误的两种方式以及reject回调后的错误捕获的一种方式

 1. Promise.prototype.then() reject回调函数
 2. Promise.prototype.catch()
 1. 结合async/awaittry...catch

4.4.1 Promise.prototype.then()捕获-- reject回调函数

特点:

1.无法捕获resolve()回调中出现的异常,需要无限链式调用then回调去捕获异常
2.无法中断后续的then操作

案例1能够在第二个reject回调中捕获到,如果又有报错,能在下一个then里面的reject回调捕获到

   const createPromise = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('promise')
      }, 1000)
    })
    createPromise.then(res => {
      console.log(res, 'resolved');
    }, res => {
      console.log(res, 'reject');
      throw new Error('reject1')
    }).then(null, res=> {
      console.log(res, 'reject2');
    })

案例2 如果已经是resove的回调了,中间有报错,也能够在下一个then里面的reject回调能捕获到

   const createPromise = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('promise')
      }, 1000)
    })
    createPromise.then(res => {
      // 结果1
      console.log(res, 'resolved');
      window.someNonexistentFunction()
    }, res => {
      console.log(res, 'reject');
    }).then(null, res=> {
      console.log(res, 'reject');
    })

案例3 能够在reject的回调函数里面去使用try catch,因为这里面就是同步代码,但是不用直接去捕获 Promise.reject()

      const createPromise = new Promise((resolve, reject) => {
        setTimeout(() => {
          reject('promise')
        }, 1000)
      })
      createPromise.then(res => {
        console.log(res, 'resolved');
      }, res => {
        console.log(res, 'reject');
        try {
          window.test()
        } catch(e) {
          console.log(e, 'e');
        }
      }).then(null, res=> {
        console.log(res, 'reject2');
      })

4.4.2 Promise.prototype.catch()捕获异常

特点:

因为Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
      catch()方法返回的还是一个 Promise 对象,因此后面还可以接着调用then()方法。

记住一点: Promise.prototype.catch() 方法用于给期约添加拒绝处理程序, 这个方法只接收一个参数:onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype.then(null, onRejected)

下面这两个是相等的:

let p = Promise.reject();
let onRejected = function(e) {
  setTimeout(console.log, 0, 'rejected');
};
// 这两种添加拒绝处理程序的方式是一样的:
p.then(null, onRejected); // rejected
p.catch(onRejected); // rejected

案例1 冒泡性质的抛错

   const createPromise = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('promise')
      }, 1000)
    })
    createPromise.then((res) => {
      console.log(res, 'resolved')
    }).then(() => {
      console.log(res, 'resolved')
    }).then(() => {
      console.log(res, 'resolved')
    }).then(() => {
      console.log(res, 'resolved')
    }, res => {
      console.log(res, 'reject')
    })

案例2 catch也同reject回调一致,具有捕获错误的冒泡性质,只要有报错,不管经过创建多少个新的promise,后面仍然能捕获到

   const createPromise = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('promise')
      }, 1000)
    })
    createPromise.then((res) => {
      console.log(res, 'resolved')
    }).then(() => {
      console.log(res, 'resolved')
    }).then(() => {
      console.log(res, 'resolved')
    }).catch((res) => {
      console.log(res, 'catch reject');
    })

案例3 catch 到了错误,又是一个新的promise,cath回调里面没有报错,下一个then还是走resolved的回调

   const createPromise = new Promise((resolve, reject) => {
       reject('promise');
    })
    createPromise.then(res => {
      console.log('resolved1', res)
    })
    .then(res => {
      console.log('resolved2', res)
    })
    .catch(res=> {
      console.log('catche reject', res)
    })
    .then(res => {
      console.log('resolved3', res)
    })

4.4.3 async/ await结合try...catch使用

什么是async函数?

1. async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
2. async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,
等到异步操作完成,再接着执行函数体内后面的语句。

什么是await命令?有那些特点

1. await只能在异步函数async function中使用。
2. await表达式会暂停当前async function的执行,等待Promise处理完成。若Promise正常处理(fulfilled),
其回调的resolve函数参数作为await表达式的值,继续执行async function3.Promise处理异常(rejected),await表达式会把Promise的异常原因抛出。
4. await如果返回的是reject状态的promise,如果不被捕获抛出,就会中断async函数的执行。
5. 另外,如果await操作符后的表达式的值不是一个Promise,则返回该值本身。
6.async await函数中,通常使用try/catch块来捕获错误。

用大白话来说 await之后的返回值就是 resolve和reject回调的结果

案例1 await 已经返回了reject()之后reject回调的结果,但是拿到结果并没有进行任何的操作 这时候你发现了没,下面吗的console.log(res, 'await result');已经不执行了,对于调用一个请求,失败了,就要中断我们所有的代码执行,显然不是我们想要的结果,那么如何让调用接口失败,我们仍然可以继续操作呢?

   const createPromise = new Promise((resove, reject) => {
      setTimeout(() => {
        console.log('我反正进来了')
        reject('promise')
      }, 1000)
    })
    async function awaitTest() {
      const res = await createPromise
      console.log(res, 'await result');
    }
    awaitTest()

案例2 对结果进行try catch

  const createPromise = new Promise((resove, reject) => {
      setTimeout(() => {
        console.log('我反正进来了')
        reject('promise')
      }, 1000)
    })
    async function awaitTest() {
      try {
        const res = await createPromise
      } catch(e) {
        console.log(e, 'await调用报错咯!');
      } 
      console.log('可以让我执行一下吗?');
    }
    awaitTest()

有没有想过一个问题? 虽然说await会返回reject回调里面的结果,或者是resolve回调里面的结果,那我try catch怎么就知道返回的东西是要报错的呢? 为啥reject返回的我就要报错呢?这里我又发现了一个好东西:

当promise函数里面调用reject()来落定拒绝契约的时候,实际上以下三个等价

throw new Error('test');
 try {
   throw new Error('test');
 } catch(e) {
   reject(e);
 }

等价于

reject(new Error('test'));

那么 try catch 肯定能捕获到 throw new Error的错误拉~ 请看来自es6的解释

1629711374(1).png

案例3 reject(new Error())与throw new Error('我是个错误,没有reject()')在什么情况下等价

const createPromise = new Promise((resove, reject) => { reject(new Error()) })

 createPromise.then((res) => { 
     console.log(res, 'resolved'); }
 ).catch(res => { console.log(res, 'catch'); })

const createPromise = new Promise((resove, reject) => { reject(new Error()) })

 createPromise.then((res) => { 
     throw new Error('我是个错误,没有reject()')}
 ).catch(res => { console.log(res, 'catch'); })

1630242127(1).png

以上两种情况都是能够通过Promise.catch()捕获到错误的,也就是对应es6上面说的相等的情况,但是这里有个前提,是在new Promise里面没有异步的情况下.

那么为什么throw new Error('我是个错误,没有reject()')与reject(new Error())相等?

源码解析:segmentfault.com/a/119000001…

1630242265(1).png

源码中,会对传入的函数进行try catch.如果是throw new Error('我是个错误,没有reject()')必定会被catch到错误,这里时候catch里面自动调用了reject().也就是直接调用了拒绝的落定,而上面说过了try,catch里面的catch是不能catch到setTimeOut里面的错误的

const createPromise = new Promise((resove, reject) =>
{ setTimeout(()=> { reject() }, 1000) }) 
createPromise.then((res) => { console.log(res, 'resolved'); })
.catch(res => { console.log(res, 'catch'); })

1630242445(1).png

const createPromise = new Promise((resove, reject) => { setTimeout(()=> 
 { throw new Error('我是个错误,没有reject()') 
}, 1000) })

createPromise.then((res) => { console.log(res, 'resolved'); })
.catch(res => { console.log(res, 'catch'); })

1630242490(1).png

为什么上面的有异步的reject()能捕获到,throw new Error('我是个错误,没有reject()')却不能promise.catch()到呢

上面提到了,源码中的第一个try catch是捕获不到setTimeOut的,里面有_state 四个状态

_state === 0 // pending 
state === 1 // fulfilled,执行了resolve函数,并且_value instanceof Promise === true
_state === 2 // rejected,执行了reject函数 
_state === 3 // fulfilled,执行了resolve函数,并且_value instanceof Promise === false

如果没有调用resolve函数或者reject函数.始终是_state === 0的penging状态.没有setTimeOut的时候是try catch到了throw new Error()做了自动的reject(),而如果有setTimeOut第一个try catch已经对里面的setTimeOut起不了作用了,所以_state 一直是等于0,而源码里面针对new Promise做了处理:

1630242850(1).png

也就说是 存在setTimeOut,会出现一个等待reject()或者resolve()调用的pending状态,如果里面没有出现一直处于pending状态,也就不能被promise.catch()到

这里最重要的要理解,try catch是不可能catch到异步的,try catch与promise.catch()是不一样的,如果promise.catch()不到错误,应该去看看promise源码对promise.catch()(即Promise.prototype.then(null, onRejected))内部是如何实现的

源码必定都是遵循js事件轮询机制的!

案例4 同上,await都拿不到结果,那就更加别提能用try catch能捕获了, await表示的是落定状态(fulfilled)返回的结果

   const createPromise = new Promise((resove, reject) => {
      setTimeout(() => {
        throw new Error('我是个错误,但是我并不想落定状态,没有reject()')
      }, 1000)
    })
    async function awaitTest() {
      try {
        console.log('测试1')
        const res = await createPromise // 调用的时候内部已经中止了,也不是settled的状态,所以没法去返回结果,那就更不可能进行到赋值给res的操作了
        console.log(res, '测试2')
      } catch(e) {
        console.log(e, 'await调用报错咯!');
      } 
      console.log('可以让我执行一下吗?');
    }
    awaitTest()

案例5 try catch 面对多个await捕获的问题

  const createPromise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('失败1')
      }, 1000)
    })
    const createPromise2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('失败2')
      }, 1000)
    })
    const createPromise3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('成功3')
      }, 1000)
    })
    async function awaitTest() {
      try {
        await createPromise1
        console.log('不执行');
        await createPromise2
        await createPromise3
      } catch(e) {
        console.log(e, 'await调用报错咯!');
      } 
      console.log('1可以让我执行一下吗?');
    }
    awaitTest()
    console.log('2可以让我执行一下吗?');

结果就是多个await 有几个报错,她只会catch到第一个,走了catch,但是仍然可以继续运行

但是我们依旧推荐多个awiat的东西如果没有关联性,考虑到并发性,可以直接用promise.all

  const createPromise1 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('失败1')
      }, 1000)
    })
    const createPromise2 = new Promise((resolve, reject) => {
      setTimeout(() => {
        reject('失败2')
      }, 1000)
    })
    const createPromise3 = new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve('成功3')
      }, 1000)
    })
    async function awaitTest() {
      const list = await Promise.all([createPromise1.catch((e) => e),
      createPromise2.catch((e) => e), createPromise3.catch((e) => e)])
      
      console.log(list, 'list');
      console.log('1可以让我执行一下吗?');
    }
    awaitTest()
    console.log('2可以让我执行一下吗?');
    

4.5 try catch不能捕获到promise.resolve()的报错

上面有提到,try catch 如果分为在主线程的执行的前中后,那么只有中才能捕获到错误,其实4.3的例子已经很明显了(4.4主要还是想大家普及一下promise捕获错误的知识)但是这边还是再讲解一下

我们先看看js高级程序设计第11章

4.5.1 try catch捕获throw new Error 这个没有问题

     try { 
        throw new Error('foo'); 
      } catch(e) { 
        console.log(e, 'catch'); 
      }

4.5.2 try catch 捕获不到 Promise.reject()

   try { 
      Promise.reject()
    } catch(e) { 
      console.log(e, 'catch'); 
    }

首先你要知道,下面这两个契约实例其实是一样的

let p1 = new Promise((resolve, reject) => reject());
let p2 = Promise.reject();

因为p1,p2是异步执行模式,而try catch是同步代码,同步代码先执行,异步去task queue排队,等同步做完再执行,而你能够在then的回调里面进行try catch或者await的返回数,只是对返回结果进行捕获而已,并捕获不了promise的异步代码

你可以将p1,p2 看成在请求,请求成功我就写告诉用户已经成功了,用resolve()告诉大家, 只有在then接受到,我可以对then里面的结果进行try catch

五 try catch啥时候用,以及如何抛错

5.1 throw new Error()

与 try/catch 语句对应的一个机制是 throw 操作符,用于在任何时候抛出自定义错误。throw 操作符必须有一个值,但值的类型不限。 使用 throw 操作符时,代码立即停止执行,除非 try/catch 语句捕获了抛出的值

  try { 
    window.someNonexistentFunction()
  } catch(e) { 
    throw new Error('报错了')
    console.log('我要执行')
  }

   try { 
        window.someNonexistentFunction()
      } catch(e) { 
        try {
          throw new Error('报错了')
        } catch(e) {
          console.log('我要执行')
        } 
      }

5.2 应该仔细评估每个函数,以及可能导致它们失败的情形。良好的错误处理协议可以保证只会发生你自己抛出的错误。

比如平时utils里面,如果传入的参数不是规定的,可以直接通过if判断类型,然后抛错

至于抛出错误与捕获错误的区别,可以这样想:应该只在确切知道接下来该做什么的时候捕获错误。捕获错误的目的是阻止浏览器以其默认方式响应;抛出错误的目的是为错误提供有关其发生原因的

5.3 通过继承自定义错误类型,与提示

class CustomError extends Error {
    constructor(message) {
        super(message);
        this.name = "CustomError";
        this.message = message;
    }
}
throw new CustomError("My message");

六 有哪些错误类型

1 Error 
2 InternalError
3 EvalError
4 RangeError
5 ReferenceError
6 SyntaxError
7 TypeError
8 URIError

1.Error 是基类型,其他错误类型继承该类型。因此,所有错误类型都共享相同的属性, 浏览器很少会抛出 Error 类型的错误,该类型主要用于开发者抛出自定义错误

  1. InternalError 该错误在JS引擎内部发生,特别是当它有太多数据要处理并且堆栈增长超过其关键限制时。示例场景通常为某些成分过大
该错误在JS引擎内部发生,特别是当它有太多数据要处理并且堆栈增长超过其关键限制时。
示例场景通常为某些成分过大,例如:
“too many switch cases”(过多case子句);
“too many parentheses in regular expression”(正则表达式中括号过多);
“array initializer too large”(数组初始化器过大);
“too much recursion”(递归过深)。
  1. EvalError 类型的错误会在使用 eval() 函数发生异常时抛出,如果非法调用 eval(),则抛出 EvalError 异常

4.RangeError 错误会在数值越界时抛出

new Array(-20)  // 异常的捕获.html:52 Uncaught RangeError: Invalid array length
const num = 1
let res = num.toFixed(-1)
console.log(res); // 异常的捕获.html:53 Uncaught RangeError: toFixed() digits argument must be between 0 and 100

5.ReferenceError 会在找不到对象时发生。(这就是著名的 "object expected" 浏览器错误的原因。)这种错误经常是由访问不存在的变量而导致的

console.log(num)  //异常的捕获.html:56 Uncaught ReferenceError: num is not defined

6.TypeError 在 JavaScript 中很常见,主要发生在变量不是预期类型,或者访问不存在的方法时

let o = new 10; // 抛出 TypeError
console.log("name" in true); // 抛出 TypeError
Function.prototype.toString.call("name"); // 抛出 TypeError

7.URIError,只会在使用 encodeURI()或 decodeURI()但传入了格式错误的URI 时发生

七 错误识别

错误处理非常重要的部分是首先识别错误可能会在代码中的什么地方发生
 类型转换错误
 数据类型错误
 通信错误
1. 使用==== ,而不使用==
2. if 判断的条件,尽量避免类型转换
3.通信错误,主要是url传参(主要错误是 URL 格式或发送数据的格式不正确,在把数据发送到服务器之前没有用,encodeURIComponent() 编码))

封装url 传参

function addQueryStringArg(url, name, value) {
    if (url.indexOf("?") == -1){
        url += "?";
    } else {
        url += "&";
    }
    url += '${encodeURIComponent(name)=${encodeURIComponent(value)}';
    return url;
}
const url = "http://www.somedomain.com";
const newUrl = addQueryStringArg(url, "redir","http://www.someotherdomain.com?a=b&c=d");
console.log(newUrl);

八 区分重大与非重大错误、

非重大错误和重大错误的区别主要体现在对用户的影响上

非重大错误
 不会影响用户的主要任务;
 只会影响页面中某个部分;
 可以恢复;
 重复操作可能成功。

重大错误
 应用程序绝对无法继续运行;
 错误严重影响了用户的主要目标;
 会导致其他错误发生。
for (let mod of mods){
   mod.init(); // 可能的重大错误
}

可以使用

for (let mod of mods){
    try {
        mod.init();
    } catch (ex){
        // 在这里处理错误
    }
}

可以封装方法来区分重大错误和非重大错误

 function logError(sev, msg) {
    let img = new Image(),
    encodedSev = encodeURIComponent(sev),
    encodedMsg = encodeURIComponent(msg);
    img.src = 'log.php?sev=${encodedSev}&msg=${encodedMsg}';
 }

logError()函数接收两个参数:严重程度和错误消息。严重程度可以是数值或字符串,具体取决于使用的日志系统。这里使用 Image 对象发送请求主要是从灵活性方面考虑的

九 调试技术

所有主流浏览器都有 JavaScript 控制台,该控制台可用于查询 JavaScript 错误。另外,这些浏览器都支持通过 console 对象直接把 JavaScript 消息写入控制台

9.1 debugger 关键字

推荐: www.cnblogs.com/xiaoqi2018/…

9.2 在页面中打印消息

平时我们看信息,有时候会在template里面去打印

9.3 根据需求封装console.log()

例如,平时我们要在浏览器上打印很多对象,但是每次都要去一级一级打开,可以去重写全局console.log (这里我没有做类型判断哦)

重写前:

console.log({data: { name: 1, age: 18 }})

重写后

    const consoleog = window.console.log
    console.log = function() {
      const args = JSON.stringify(...arguments)
      consoleog(args)
    }
    console.log({data: { name: 1, age: 18 }})

十 抛出错误

抛出错误是调试代码的很好方式。如果错误消息足够具体,只要看一眼错误就可以确定原因。好的错误消息包含关于错误原因的确切信息,因此可以减少额外调试的工作量

function divide(num1, num2) {
    if (typeof num1 != "number" || typeof num2 != "number"){
        throw new Error("divide(): Both arguments must be numbers.");
    }
    return num1 / num2;
}

终于写完了,不好的地方请指点

做一天前端,就好好努力沉淀吧~