根据笔者的项目经验,本文讲解了从函数回调,到 es7
规范的异常处理方式。异常处理的优雅性随着规范的进步愈来愈高,不要惧怕使用 try catch
,不能回避异常处理。javascript
咱们须要一个健全的架构捕获全部同步、异步的异常。业务方不处理异常时,中断函数执行并启用默认处理,业务方也能够随时捕获异常本身处理。前端
优雅的异常处理方式就像冒泡事件,任何元素能够自由拦截,也能够听任无论交给顶层处理。java
文字讲解仅是背景知识介绍,不包含对代码块的完整解读,不要忽略代码块的阅读。node
若是在回调函数中直接处理了异常,是最不明智的选择,由于业务方彻底失去了对异常的控制能力。git
下方的函数 请求处理
不但永远不会执行,还没法在异常时作额外的处理,也没法阻止异常产生时笨拙的 console.log('请求失败')
行为。github
function fetch(callback) {
setTimeout(() => {
console.log('请求失败')
})
}
fetch(() => {
console.log('请求处理') // 永远不会执行
})复制代码
回调函数有同步和异步之分,区别在于对方执行回调函数的时机,异常通常出如今请求、数据库链接等操做中,这些操做大可能是异步的。golang
异步回调中,回调函数的执行栈与原函数分离开,致使外部没法抓住异常。数据库
从下文开始,咱们约定用
setTimeout
模拟异步操做promise
function fetch(callback) {
setTimeout(() => {
throw Error('请求失败')
})
}
try {
fetch(() => {
console.log('请求处理') // 永远不会执行
})
} catch (error) {
console.log('触发异常', error) // 永远不会执行
}
// 程序崩溃
// Uncaught Error: 请求失败复制代码
咱们变得谨慎,不敢再随意抛出异常,这已经违背了异常处理的基本原则。浏览器
虽然使用了 error-first
约定,使异常看起来变得可处理,但业务方依然没有对异常的控制权,是否调用错误处理取决于回调函数是否执行,咱们没法知道调用的函数是否可靠。
更糟糕的问题是,业务方必须处理异常,不然程序挂掉就会什么都不作,这对大部分不用特殊处理异常的场景形成了很大的精神负担。
function fetch(handleError, callback) {
setTimeout(() => {
handleError('请求失败')
})
}
fetch(() => {
console.log('失败处理') // 失败处理
}, error => {
console.log('请求处理') // 永远不会执行
})复制代码
Promise
是一个承诺,只多是成功、失败、无响应三种状况之一,一旦决策,没法修改结果。
Promise
不属于流程控制,但流程控制能够用多个 Promise
组合实现,所以它的职责很单一,就是对一个决议的承诺。
resolve
代表经过的决议,reject
代表拒绝的决议,若是决议经过,then
函数的第一个回调会当即插入 microtask
队列,异步当即执行。
简单补充下事件循环的知识,js 事件循环分为 macrotask 和 microtask。
microtask 会被插入到每个 macrotask 的尾部,因此 microtask 总会优先执行,哪怕 macrotask 由于 js 进程繁忙被 hung 住。
好比setTimeout
setInterval
会插入到 macrotask 中。
const promiseA = new Promise((resolve, reject) => {
resolve('ok')
})
promiseA.then(result => {
console.log(result) // ok
})复制代码
若是决议结果是决绝,那么 then
函数的第二个回调会当即插入 microtask
队列。
const promiseB = new Promise((resolve, reject) => {
reject('no')
})
promiseB.then(result => {
console.log(result) // 永远不会执行
}, error => {
console.log(error) // no
})复制代码
若是一直不决议,此 promise
将处于 pending
状态。
const promiseC = new Promise((resolve, reject) => {
// nothing
})
promiseC.then(result => {
console.log(result) // 永远不会执行
}, error => {
console.log(error) // 永远不会执行
})复制代码
未捕获的 reject
会传到末尾,经过 catch
接住
const promiseD = new Promise((resolve, reject) => {
reject('no')
})
promiseD.then(result => {
console.log(result) // 永远不会执行
}).catch(error => {
console.log(error) // no
})复制代码
resolve
决议会被自动展开(reject
不会)
const promiseE = new Promise((resolve, reject) => {
return new Promise((resolve, reject) => {
resolve('ok')
})
})
promiseE.then(result => {
console.log(result) // ok
})复制代码
链式流,then
会返回一个新的 Promise
,其状态取决于 then
的返回值。
const promiseF = new Promise((resolve, reject) => {
resolve('ok')
})
promiseF.then(result => {
return Promise.reject('error1')
}).then(result => {
console.log(result) // 永远不会执行
return Promise.resolve('ok1') // 永远不会执行
}).then(result => {
console.log(result) // 永远不会执行
}).catch(error => {
console.log(error) // error1
})复制代码
不只是 reject
,抛出的异常也会被做为拒绝状态被 Promise
捕获。
function fetch(callback) {
return new Promise((resolve, reject) => {
throw Error('用户不存在')
})
}
fetch().then(result => {
console.log('请求处理', result) // 永远不会执行
}).catch(error => {
console.log('请求处理异常', error) // 请求处理异常 用户不存在
})复制代码
可是,永远不要在 macrotask
队列中抛出异常,由于 macrotask
队列脱离了运行上下文环境,异常没法被当前做用域捕获。
function fetch(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
throw Error('用户不存在')
})
})
}
fetch().then(result => {
console.log('请求处理', result) // 永远不会执行
}).catch(error => {
console.log('请求处理异常', error) // 永远不会执行
})
// 程序崩溃
// Uncaught Error: 用户不存在复制代码
不过 microtask
中抛出的异常能够被捕获,说明 microtask
队列并无离开当前做用域,咱们经过如下例子来证实:
Promise.resolve(true).then((resolve, reject)=> {
throw Error('microtask 中的异常')
}).catch(error => {
console.log('捕获异常', error) // 捕获异常 Error: microtask 中的异常
})复制代码
至此,Promise
的异常处理有了比较清晰的答案,只要注意在 macrotask
级别回调中使用 reject
,就没有抓不住的异常。
若是第三方函数在 macrotask
回调中以 throw Error
的方式抛出异常怎么办?
function thirdFunction() {
setTimeout(() => {
throw Error('就是任性')
})
}
Promise.resolve(true).then((resolve, reject) => {
thirdFunction()
}).catch(error => {
console.log('捕获异常', error)
})
// 程序崩溃
// Uncaught Error: 就是任性复制代码
值得欣慰的是,因为不在同一个调用栈,虽然这个异常没法被捕获,但也不会影响当前调用栈的执行。
咱们必须正视这个问题,惟一的解决办法,是第三方函数不要作这种傻事,必定要在 macrotask
抛出异常的话,请改成 reject
的方式。
function thirdFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('收敛一些')
})
})
}
Promise.resolve(true).then((resolve, reject) => {
return thirdFunction()
}).catch(error => {
console.log('捕获异常', error) // 捕获异常 收敛一些
})复制代码
请注意,若是 return thirdFunction()
这行缺乏了 return
的话,依然没法抓住这个错误,这是由于没有将对方返回的 Promise
传递下去,错误也不会继续传递。
咱们发现,这样还不是完美的办法,不但容易忘记 return
,并且当同时含有多个第三方函数时,处理方式不太优雅:
function thirdFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('收敛一些')
})
})
}
Promise.resolve(true).then((resolve, reject) => {
return thirdFunction().then(() => {
return thirdFunction()
}).then(() => {
return thirdFunction()
}).then(() => {
})
}).catch(error => {
console.log('捕获异常', error)
})复制代码
是的,咱们还有更好的处理方式。
generator
是更为优雅的流程控制方式,可让函数可中断执行:
function* generatorA() {
console.log('a')
yield
console.log('b')
}
const genA = generatorA()
genA.next() // a
genA.next() // b复制代码
yield
关键字后面能够包含表达式,表达式会传给 next().value
。
next()
能够传递参数,参数做为 yield
的返回值。
这些特性足以孕育出伟大的生成器,咱们稍后介绍。下面是这个特性的例子:
function* generatorB(count) {
console.log(count)
const result = yield 5
console.log(result * count)
}
const genB = generatorB(2)
genB.next() // 2
const genBValue = genB.next(7).value // 14
// genBValue undefined复制代码
第一个 next 是没有参数的,由于在执行 generator
函数时,初始值已经传入,第一个 next
的参数没有任何意义,传入也会被丢弃。
const result = yield 5复制代码
这一句,返回值不是想固然的 5
。其的做用是将 5
传递给 genB.next()
,其值,由下一个 next genB.next(7)
传给了它,因此语句等于 const result = 7
。
最后一个 genBValue
,是最后一个 next
的返回值,这个值,就是函数的 return
值,显然为 undefined
。
咱们回到这个语句:
const result = yield 5复制代码
若是返回值是 5,是否是就清晰了许多?是的,这种语法就是 await
。因此 Async Await
与 generator
有着莫大的关联,桥梁就是 生成器,咱们稍后介绍 生成器。
若是认为 Generator
不太好理解,那 Async Await
绝对是救命稻草,咱们看看它们的特征:
const timeOut = (time = 0) => new Promise((resolve, reject) => {
setTimeout(() => {
resolve(time + 200)
}, time)
})
async function main() {
const result1 = await timeOut(200)
console.log(result1) // 400
const result2 = await timeOut(result1)
console.log(result2) // 600
const result3 = await timeOut(result2)
console.log(result3) // 800
}
main()复制代码
所见即所得,await
后面的表达式被执行,表达式的返回值被返回给了 await
执行处。
可是程序是怎么暂停的呢?只有 generator
能够暂停程序。那么等等,回顾一下 generator
的特性,咱们发现它也能够达到这种效果。
终于能够介绍 生成器 了!它能够魔法般将下面的 generator
执行成为 await
的效果。
function* main() {
const result1 = yield timeOut(200)
console.log(result1)
const result2 = yield timeOut(result1)
console.log(result2)
const result3 = yield timeOut(result2)
console.log(result3)
}复制代码
下面的代码就是生成器了,生成器并不神秘,它只有一个目的,就是:
所见即所得,
yield
后面的表达式被执行,表达式的返回值被返回给了yield
执行处。
达到这个目标不难,达到了就完成了 await
的功能,就是这么神奇。
function step(generator) {
const gen = generator()
// 因为其传值,返回步骤交错的特性,记录上一次 yield 传过来的值,在下一个 next 返回过去
let lastValue
// 包裹为 Promise,并执行表达式
return () => Promise.resolve(gen.next(lastValue).value).then(value => {
lastValue = value
return lastValue
})
}复制代码
利用生成器,模拟出 await
的执行效果:
const run = step(main)
function recursive(promise) {
promise().then(result => {
if (result) {
recursive(promise)
}
})
}
recursive(run)
// 400
// 600
// 800复制代码
能够看出,await
的执行次数由程序自动控制,而回退到 generator
模拟,须要根据条件判断是否已经将函数执行完毕。
不管是同步、异步的异常,await
都不会自动捕获,但好处是能够自动中断函数,咱们大可放心编写业务逻辑,而不用担忧异步异常后会被执行引起雪崩:
function fetch(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject()
})
})
}
async function main() {
const result = await fetch()
console.log('请求处理', result) // 永远不会执行
}
main()复制代码
咱们使用 try catch
捕获异常。
认真阅读 Generator
番外篇的话,就会理解为何此时异步的异常能够经过 try catch
来捕获。
由于此时的异步其实在一个做用域中,经过 generator
控制执行顺序,因此能够将异步看作同步的代码去编写,包括使用 try catch
捕获异常。
function fetch(callback) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('no')
})
})
}
async function main() {
try {
const result = await fetch()
console.log('请求处理', result) // 永远不会执行
} catch (error) {
console.log('异常', error) // 异常 no
}
}
main()复制代码
和第五章 Promise 没法捕获的异常 同样,这也是 await
的软肋,不过任然能够经过第六章的方案解决:
function thirdFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('收敛一些')
})
})
}
async function main() {
try {
const result = await thirdFunction()
console.log('请求处理', result) // 永远不会执行
} catch (error) {
console.log('异常', error) // 异常 收敛一些
}
}
main()复制代码
如今解答第六章尾部的问题,为何 await
是更加优雅的方案:
async function main() {
try {
const result1 = await secondFunction() // 若是不抛出异常,后续继续执行
const result2 = await thirdFunction() // 抛出异常
const result3 = await thirdFunction() // 永远不会执行
console.log('请求处理', result) // 永远不会执行
} catch (error) {
console.log('异常', error) // 异常 收敛一些
}
}
main()复制代码
在现在 action
概念成为标配的时代,咱们大能够将全部异常处理收敛到 action
中。
咱们以以下业务代码为例,默认不捕获错误的话,错误会一直冒泡到顶层,最后抛出异常。
const successRequest = () => Promise.resolve('a')
const failRequest = () => Promise.reject('b')
class Action {
async successReuqest() {
const result = await successRequest()
console.log('successReuqest', '处理返回值', result) // successReuqest 处理返回值 a
}
async failReuqest() {
const result = await failRequest()
console.log('failReuqest', '处理返回值', result) // 永远不会执行
}
async allReuqest() {
const result1 = await successRequest()
console.log('allReuqest', '处理返回值 success', result1) // allReuqest 处理返回值 success a
const result2 = await failRequest()
console.log('allReuqest', '处理返回值 success', result2) // 永远不会执行
}
}
const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()
// 程序崩溃
// Uncaught (in promise) b
// Uncaught (in promise) b复制代码
为了防止程序崩溃,须要业务线在全部 async 函数中包裹 try catch
。
咱们须要一种机制捕获 action
最顶层的错误进行统一处理。
为了补充前置知识,咱们再次进入番外话题。
Decorator
中文名是装饰器,核心功能是能够经过外部包装的方式,直接修改类的内部属性。
装饰器按照装饰的位置,分为 class decorator
method decorator
以及 property decorator
(目前标准还没有支持,经过 get
set
模拟实现)。
类级别装饰器,修饰整个类,能够读取、修改类中任何属性和方法。
const classDecorator = (target: any) => {
const keys = Object.getOwnPropertyNames(target.prototype)
console.log('classA keys,', keys) // classA keys ["constructor", "sayName"]
}
@classDecorator
class A {
sayName() {
console.log('classA ascoders')
}
}
const a = new A()
a.sayName() // classA ascoders复制代码
方法级别装饰器,修饰某个方法,和类装饰器功能相同,可是能额外获取当前修饰的方法名。
为了发挥这一特色,咱们篡改一下修饰的函数。
const methodDecorator = (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
return {
get() {
return () => {
console.log('classC method override')
}
}
}
}
class C {
@methodDecorator
sayName() {
console.log('classC ascoders')
}
}
const c = new C()
c.sayName() // classC method override复制代码
属性级别装饰器,修饰某个属性,和类装饰器功能相同,可是能额外获取当前修饰的属性名。
为了发挥这一特色,咱们篡改一下修饰的属性值。
const propertyDecorator = (target: any, propertyKey: string | symbol) => {
Object.defineProperty(target, propertyKey, {
get() {
return 'github'
},
set(value: any) {
return value
}
})
}
class B {
@propertyDecorator
private name = 'ascoders'
sayName() {
console.log(`classB ${this.name}`)
}
}
const b = new B()
b.sayName() // classB github复制代码
咱们来编写类级别装饰器,专门捕获 async
函数抛出的异常:
const asyncClass = (errorHandler?: (error?: Error) => void) => (target: any) => {
Object.getOwnPropertyNames(target.prototype).forEach(key => {
const func = target.prototype[key]
target.prototype[key] = async (...args: any[]) => {
try {
await func.apply(this, args)
} catch (error) {
errorHandler && errorHandler(error)
}
}
})
return target
}复制代码
将类全部方法都用 try catch
包裹住,将异常交给业务方统一的 errorHandler
处理:
const successRequest = () => Promise.resolve('a')
const failRequest = () => Promise.reject('b')
const iAsyncClass = asyncClass(error => {
console.log('统一异常处理', error) // 统一异常处理 b
})
@iAsyncClass
class Action {
async successReuqest() {
const result = await successRequest()
console.log('successReuqest', '处理返回值', result)
}
async failReuqest() {
const result = await failRequest()
console.log('failReuqest', '处理返回值', result) // 永远不会执行
}
async allReuqest() {
const result1 = await successRequest()
console.log('allReuqest', '处理返回值 success', result1)
const result2 = await failRequest()
console.log('allReuqest', '处理返回值 success', result2) // 永远不会执行
}
}
const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()复制代码
咱们也能够编写方法级别的异常处理:
const asyncMethod = (errorHandler?: (error?: Error) => void) => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const func = descriptor.value
return {
get() {
return (...args: any[]) => {
return Promise.resolve(func.apply(this, args)).catch(error => {
errorHandler && errorHandler(error)
})
}
},
set(newValue: any) {
return newValue
}
}
}复制代码
业务方用法相似,只是装饰器须要放在函数上:
const successRequest = () => Promise.resolve('a')
const failRequest = () => Promise.reject('b')
const asyncAction = asyncMethod(error => {
console.log('统一异常处理', error) // 统一异常处理 b
})
class Action {
@asyncAction async successReuqest() {
const result = await successRequest()
console.log('successReuqest', '处理返回值', result)
}
@asyncAction async failReuqest() {
const result = await failRequest()
console.log('failReuqest', '处理返回值', result) // 永远不会执行
}
@asyncAction async allReuqest() {
const result1 = await successRequest()
console.log('allReuqest', '处理返回值 success', result1)
const result2 = await failRequest()
console.log('allReuqest', '处理返回值 success', result2) // 永远不会执行
}
}
const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()复制代码
我想描述的意思是,在第 11 章这种场景下,业务方是不用担忧异常致使的 crash
,由于全部异常都会在顶层统一捕获,可能表现为弹出一个提示框,告诉用户请求发送失败。
业务方也不须要判断程序中是否存在异常,而战战兢兢的处处 try catch
,由于程序中任何异常都会马上终止函数的后续执行,不会再引起更恶劣的结果。
像 golang 中异常处理方式,就存在这个问题
经过 err, result := func() 的方式,虽然固定了第一个参数是错误信息,但下一行代码免不了要以if error {...}
开头,整个程序的业务代码充斥着巨量的没必要要错误处理,而大部分时候,咱们还要为如何处理这些错误想的焦头烂额。
而 js 异常冒泡的方式,在前端能够用提示框兜底,nodejs端能够返回 500 错误兜底,并马上中断后续请求代码,等于在全部危险代码身后加了一层隐藏的 return
。
同时业务方也握有绝对的主动权,好比登陆失败后,若是帐户不存在,那么直接跳转到注册页,而不是傻瓜的提示用户账号不存在,能够这样作:
async login(nickname, password) {
try {
const user = await userService.login(nickname, password)
// 跳转到首页,登陆失败后不会执行到这,因此不用担忧用户看到奇怪的跳转
} catch (error) {
if (error.no === -1) {
// 跳转到登陆页
} else {
throw Error(error) // 其余错误不想管,把球继续踢走
}
}
}复制代码
在 nodejs
端,记得监听全局错误,兜住落网之鱼:
process.on('uncaughtException', (error: any) => {
logger.error('uncaughtException', error)
})
process.on('unhandledRejection', (error: any) => {
logger.error('unhandledRejection', error)
})复制代码
在浏览器端,记得监听 window
全局错误,兜住漏网之鱼:
window.addEventListener('unhandledrejection', (event: any) => {
logger.error('unhandledrejection', event)
})
window.addEventListener('onrejectionhandled', (event: any) => {
logger.error('onrejectionhandled', event)
})复制代码
若有错误,欢迎斧正,本人 github 主页:github.com/ascoders 但愿结交有识之士!