让 promise 在发生异常时 中断代码执行的三种方式

3,352 阅读6分钟

看过很多讲 promiseasync 的文章,但是一直看一直忘,于是打算弄几句口诀来帮助记忆;另外,结合自己的一些项目经验,谈一下,async 在实际项目中如何更优雅的使用

口诀:

  1. 如果.then内部所指定的成功或者失败的回调函数的返回值,是一个非promise值,那么外层的这个.then 所返回的promise状态就为成功的,值就为返回的非promise值

2.如果返回的本身就是一个promise值, 那么就把这个值作为外侧.then 所返回的promise实例的值,如果返回一个pedding状态的promise, 那么外侧的.then,也会返回一个pedding状态的promise实例(用于中断promise),如果.then的回调函数里面里面抛出了异常,那么.then所返回的promise的状态就为失败的, 失败的原因就为抛出的异常

  1. await 只能等待成功的promise

先从 promise 说起

show me the code

fetch('https://jsonplaceholder.typicode.com/todos/1')
 .then(
 response => {console.log('联系服务器成功了',response)},
 error => {console.log('联系服务器失败了',error)}
 )

什么情况下会触发第一个 error

首先,思考一个问题, 什么时候会触发上面这个 error 回调?

  • 只有联系服务器不成功,才会触发,比如断网,超时

404 的时候,会触发吗?

fetch('https://jsonplaceholder.typicode.com/todos2222/1')
 .then(
 response => {console.log('联系服务器成功了',response)},
 error => {console.log('联系服务器失败了',error)}
 )

修改地址, 请求一个不存在的资源, 将看到 http 响应状态码为 404,并且还是显示 联系服务器成功了, 为什么呢?

其实,联系服务器成不成功,和服务器能不能响应 todos2222 ,是两码事。 会有这个误解的原因主要还是纯前端对于http状态的东西,只是浅显的了解。其实 返回 404,甚至返回 10000,都是后端开发人员自己写的, 只是有一个叫http状态码规范的东西, 来约束前后端开发人员罢了。 所以 返回 404, 其实还是联系上服务器了, 不同的http库,其对异常的理解也不一样, 例如axios, 所有非200状态码,都认为是异常

接下里获取数据

 fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(
  response => {
  	console.log('联系服务器成功了')
    return response.json()
  },
  error => {console.log('联系服务器失败了',error)}
  ).then(
  response => {console.log('获取数据成功了', response)},
  error => {console.log('获取数据失败了',error)}
  )

以上代码, 其实是有问题的,当联系服务器失败时, 还是会打印获取数据成功了, 这样的代码是不能放到实际业务中的。比如: 有个充值的异步任务, 充值成功,才跳转到充值成功页面,而假设使用以上代码, 那么即使充值失败,也会跳转到支付成功页面。

为什么 prosime 会这样呢?

当联系服务器失败时, 程序来到失败回调: error => {console.log('联系服务器失败了',error)}, 这个回调函数会返回 undefined, 而undefined 属于非peomise值,根据上面的口诀,当 .then 中的回调返回了 非promise值 时,外层的 .then将返回成功的状态,值为 undifined, 因此会触发response => {console.log('获取数据成功了', response)}

在处理后端返回的业务数据前, 除了需要处理 axiosfetch 等api的第一个.then,也会有其他的 .then , 比如做了 promise 封装,以便清洗一下返回参数,或者做统一的错误处理:比如弹窗显示,这样可以让我们不用在每一个具体的页面里再写一遍错误弹窗逻辑,所以非常需要实现的需求就是, 在前面的.then回调中出现错误时,不要再执行之后的.then回调了

解决方法:

  1. 返回 peddingpromise 状态

  2. 把错误处理放到最后

  3. 使用 async await

返回 peddingpromise 状态

.then 返回一个 peddiing 状态的 promise

 fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(
  response => {
  	console.log('联系服务器成功了')
    return response.json()
  },
  error => {
  console.log('联系服务器失败了',error)
  return new Promise(()=>{})
  }
  ).then(
  response => {console.log('获取数据成功了', response)},
  error => {console.log('获取数据失败了',error)}
  )

ps: 记住,new Promise(()=>{}) 需要至少传入一个回调

把错误放在最后处理

promise链某个.then出现错误时,还会执行之后的.then,原因就是我们返回了非promise值,隐式的使外层的.then返回了成功的状态,以至于后面的.then中的成功回调会被调用,如果我们就不返回 非Promise值, 比如说不给.then(()=>{},()=>{})传递第二个回调函数,这样的话,.then 的状态就只由内部的第一个回调指定,但是第二个回调是处理异常的, 如果没有第二个回调,异常又怎么处理呢? 其实 promise 的错误会传递, 所以可以在最后处理错误。需要做错误的区分,有点麻烦

fetch('https://jsonplaceholder.typicode.com/todos/1')
  .then(
  response => {
  	console.log('联系服务器成功了')
    return response.json()
  },
  ).then(
  response => {console.log('获取数据成功了', response)}
  ).catch(
  error => {console.log('catch到错误:',error)}
  )
  

使用 async await

因为await 只能等待 成功的promise, 所以对于失败的promise, 会中断代码的执行,这样也达到了中断promise链的目的,之后再结合try{}catch(){}处理异常, 即使做了try catch, 代码仍然会中断在异常的promise处

const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
console.log('当网络中断时, 我不会执行')
const data = await response.json()
console.log(data)
try{
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1')
console.log('当网络中断时, 我不会执行')
const data = await response.json()
console.log(data)
}catch(err){
console.log('catch到错误:',err)
}

如果有人不喜欢使用 try{}catch(){}处理异常,也可以试试,await-to-js, 也就是 uniapppromise 的封装风格

import to from 'await-to-js';
// If you use CommonJS (i.e NodeJS environment), it should be:
// const to = require('await-to-js').default;
 
async function asyncTaskWithCb(cb) {
     let err, user, savedTask, notification;
 
     [ err, user ] = await to(UserModel.findById(1));
     if(!user) return cb('No user found');
 
     [ err, savedTask ] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
     if(err) return cb('Error occurred while saving task');
 
    if(user.notificationsEnabled) {
       [ err ] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
       if(err) return cb('Error while sending notification');
    }
 
    if(savedTask.assignedUser.id !== user.id) {
       [ err, notification ] = await to(NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you'));
       if(err) return cb('Error while sending notification');
    }
 
    cb(null, savedTask);
}
 
async function asyncFunctionWithThrow() {
  const [err, user] = await to(UserModel.findById(1));
  if (!user) throw new Error('User not found');
  
}

选择 try{}catch(){} 还是 await-to-js

await-to-js 由于在内部把异常包装成非promise值,因此只会返回成功的状态,假如 await 中发生错误,await 的下一行代码还是会执行, 所以需要在每一个 await 的下一行都做异常判断,并且使用 return 关键字 中断代码执行

我更倾向于简洁直观的代码风格 喜欢把所有 await写在一起,这样阅读起来会比较清晰,至于异常处理部分,虽然需要做if(err.type)else{} 判断, 代码会显得凌乱一些,不过只在初次编写时会凌乱,之后维护时,基本也不会看这部分的代码 所以选择 try{}catch(){}

参考:

  1. Mark Dalgleish await-to-js