Based on my project experience, this article explains how to handle exceptions from function callbacks to the ES7 specification. As the elegance of exception handling grows with the specification, don’t be afraid to use try catch and avoid exception handling.

We need a robust architecture to catch all synchronous and asynchronous exceptions. When the business side does not handle the exception, it interrupts the function execution and enables default handling. The business side can also catch the exception and handle it itself at any time.

Elegant exception handling is like bubbling events, where any element can be intercepted or left to the top level.

The text explanation is only the background, does not contain a complete interpretation of the code block, do not ignore the code block reading.

1. The callback

Handling the exception directly in the callback function is the most unwise option because the business side has no control over the exception.

Not only does the function request processing below never execute, it does not do additional processing when an exception occurs, nor does it prevent the clumsy console.log(‘ request failed ‘) behavior when an exception occurs.

function fetch(callback) {
    setTimeout((a)= > {
        console.log('Request failed')
    })
}

fetch((a)= > {
    console.log('Request processing') // Never execute
})Copy the code

2. Callbacks, exceptions that cannot be caught

Callbacks can be synchronous or asynchronous. The difference lies in the timing when the callback is executed. Exceptions usually occur in requests and database connections, which are mostly asynchronous.

In an asynchronous callback, the execution stack of the callback function is separated from the original function, making it impossible for outsiders to catch the exception.

Starting below, we agree to use setTimeout to simulate asynchronous operations

function fetch(callback) {
    setTimeout((a)= > {
        throw Error('Request failed')})}try {
    fetch((a)= > {
        console.log('Request processing') // Never execute})}catch (error) {
    console.log('Trigger exception', error) // Never execute
}

// The program crashed
// Uncaught Error: The request failsCopy the code

3. Callback, uncontrollable exception

We become wary of throwing exceptions, which violates the basic rule of exception handling.

Although the error-first convention is used to make the exception seem manageable, the business side still has no control over the exception, and calling error handling depends on whether the callback function is executed, and we have no way of knowing if the function called is reliable.

To make matters worse, the business side has to handle exceptions or else the program hangs and does nothing, which is a great mental burden for most scenarios where exceptions are not handled specifically.

function fetch(handleError, callback) {
    setTimeout((a)= > {
        handleError('Request failed')
    })
}

fetch((a)= > {
    console.log('Failure handling') // Failed processing
}, error => {
    console.log('Request processing') // Never execute
})Copy the code

Promise basis

A Promise is a Promise that can be one of three things: success, failure, or no response. Once a decision is made, the outcome cannot be changed.

Promises are not part of process control, but process control can be combined with multiple promises, so its responsibility is a single commitment to a resolution.

Resolve indicates a resolution that is passed, reject indicates a resolution that is rejected, and if the resolution is passed, the first callback of the THEN function is immediately inserted into the MicroTask queue and executed asynchronously.

Js event loop is divided into MacroTask and MicroTask. Microtasks are inserted at the end of each MacroTask, so microTasks are always executed first, even if MacroTask is stuck because js processes are busy. For example setTimeout setInterval is inserted into macroTask.

const promiseA = new Promise((resolve, reject) = > {
    resolve('ok')
})
promiseA.then(result= > {
    console.log(result) // ok
})Copy the code

If the resolution results in a reject, the second callback to the THEN function is immediately inserted into the MicroTask queue.

const promiseB = new Promise((resolve, reject) = > {
    reject('no')
})
promiseB.then(result= > {
    console.log(result) // Never execute
}, error => {
    console.log(error) // no
})Copy the code

If no decision is made, the promise is in a pending state.

const promiseC = new Promise((resolve, reject) = > {
    // nothing
})
promiseC.then(result= > {
    console.log(result) // Never execute
}, error => {
    console.log(error) // Never execute
})Copy the code

The uncaught reject passes to the end and is caught by a catch

const promiseD = new Promise((resolve, reject) = > {
    reject('no')
})
promiseD.then(result= > {
    console.log(result) // Never execute
}).catch(error= > {
    console.log(error) // no
})Copy the code

Resolve is automatically expanded (reject)

const promiseE = new Promise((resolve, reject) = > {
    return new Promise((resolve, reject) = > {
        resolve('ok')
    })
})
promiseE.then(result= > {
    console.log(result) // ok
})Copy the code

Chain flow, then returns a new Promise, the state of which depends on the return value of then.

const promiseF = new Promise((resolve, reject) = > {
    resolve('ok')
})
promiseF.then(result= > {
    return Promise.reject('error1')
}).then(result= > {
    console.log(result) // Never execute
    return Promise.resolve('ok1') // Never execute
}).then(result= > {
    console.log(result) // Never execute
}).catch(error= > {
    console.log(error) // error1
})Copy the code

4 Promise exception processing

Not only reject, exceptions thrown are also caught by Promise as a reject state.

function fetch(callback) {
    return new Promise((resolve, reject) = > {
        throw Error('User does not exist')
    })
}

fetch().then(result= > {
    console.log('Request processing', result) // Never execute
}).catch(error= > {
    console.log('Request processing exception', error) // The user does not exist
})Copy the code

5 Exceptions that Promise cannot catch

However, never throw an exception in a MacroTask queue, because the MacroTask queue is out of the running context and the exception cannot be caught by the current scope.

function fetch(callback) {
    return new Promise((resolve, reject) = > {
        setTimeout((a)= > {
             throw Error('User does not exist')
        })
    })
}

fetch().then(result= > {
    console.log('Request processing', result) // Never execute
}).catch(error= > {
    console.log('Request processing exception', error) // Never execute
})

// The program crashed
// Uncaught Error: The user does not existCopy the code

The exception thrown by microTask can be caught, indicating that the MicroTask queue does not leave the current scope. We use the following example to prove that:

Promise.resolve(true).then((resolve, reject) = > {
    throw Error('Exception in MicroTask')
}).catch(error= > {
    console.log('Catch exception', error) // Catch exception Error: exception in microTask
})Copy the code

At this point, Promise’s exception handling is fairly clear, and as long as you take care to use reject in macroTask-level callbacks, there are no exceptions that can’t be caught.

6 Promise asked

What if a third-party function throws an exception as an Error in a MacroTask callback?

function thirdFunction() {
    setTimeout((a)= > {
        throw Error('It's capricious.')})}Promise.resolve(true).then((resolve, reject) = > {
    thirdFunction()
}).catch(error= > {
    console.log('Catch exception', error)
})

// The program crashed
// Uncaught Error: Is willfulCopy the code

The good news is that while this exception cannot be caught because it is not on the same call stack, it does not affect the execution of the current call stack.

The only solution to this problem is to reject third-party functions and reject them when macroTask throws an exception.

function thirdFunction() {
    return new Promise((resolve, reject) = > {
        setTimeout((a)= > {
            reject('Let's just converge.')})})}Promise.resolve(true).then((resolve, reject) = > {
    return thirdFunction()
}).catch(error= > {
    console.log('Catch exception', error) // Catch exception convergence
})Copy the code

Note that if the return thirdFunction() line is missing a return, the error will still not be caught because the return Promise is not passed and the error will not continue.

We found that this was not a perfect solution, not only was it easy to forget the return, but it was also not very elegant when multiple third-party functions were included:

function thirdFunction() {
    return new Promise((resolve, reject) = > {
        setTimeout((a)= > {
            reject('Let's just converge.')})})}Promise.resolve(true).then((resolve, reject) = > {
    return thirdFunction().then((a)= > {
        return thirdFunction()
    }).then((a)= > {
        return thirdFunction()
    }).then((a)= > {
    })
}).catch(error= > {
    console.log('Catch exception', error)
})Copy the code

Yes, there are better ways to handle this.

Generator foundation

Generator is a more elegant form of flow control that allows functions to interrupt execution:

function* generatorA() {
    console.log('a')
    yield
    console.log('b')}const genA = generatorA()
genA.next() // a
genA.next() // bCopy the code

The yield keyword can be followed by expressions that are passed to next().value.

Next () can pass arguments, which are returned by yield.

These features are enough to make for great generators, which we’ll cover later. Here is an example of this feature:

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 undefinedCopy the code

The first next has no arguments because the initial values are passed in when the generator is executed, and the arguments to the first Next have no meaning and are discarded.

const result = yield 5Copy the code

In this case, the return value is not a given 5. It passes 5 to genb.next (), whose value is passed to it by the next next genb.next (7), so const result = 7.

The last genBValue is the return value of the last next, which is the return value of the function, which is obviously undefined.

Let’s go back to this statement:

const result = yield 5Copy the code

Wouldn’t it be much clearer if the return value was 5? Yes, this syntax is await. So Async Await has a lot to do with generator, bridge is generator, we’ll talk about generator later.

Present Async Await

If Generator is not easy to understand, Async Await is a lifesaver, let’s take a look at their features:

const timeOut = (time = 0) = > new Promise((resolve, reject) = > {
    setTimeout((a)= > {
        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()Copy the code

What YOU see is what you get. The expression after await is executed and the return value of the expression is returned to the await execution.

But how does the program pause? Only the Generator can suspend the program. So wait, reviewing the features of generator, we see that it can do the same.

The outer async await is the syntactic sugar of the generator

Finally, generators! It can magically execute the generator below to await effect.

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)
}Copy the code

The following code is a generator. Generators are not mysterious and have only one purpose:

What you see is what you get, the expression after yield is executed, and the return value of the expression is returned to the yield execution.

It is not difficult to achieve this goal, and once achieved, the function of await is completed, which is so amazing.

function step(generator) {
    const gen = generator()
    // Due to the interleaved nature of its value transfer, the last yield value is returned and the next next value is returned
    let lastValue
    // The package is a Promise and executes the expression
    return (a)= > Promise.resolve(gen.next(lastValue).value).then(value= > {
        lastValue = value
        return lastValue
    })
}Copy the code

With the generator, simulate the execution effect of await:

const run = step(main)

function recursive(promise) {
    promise().then(result= > {
        if (result) {
            recursive(promise)
        }
    })
}

recursive(run)
/ / 400
/ / 600
/ / 800Copy the code

As can be seen, the number of execution of await is controlled automatically by the program, while it falls back to generator simulation and needs to judge whether the function has been completed according to the conditions.

7 Async Await exception

We can write business logic without worrying about an avalanche of async exceptions being executed:

function fetch(callback) {
    return new Promise((resolve, reject) = > {
        setTimeout((a)= > {
            reject()
        })
    })
}

async function main() {
    const result = await fetch()
    console.log('Request processing', result) // Never execute
}

main()Copy the code

8 Async Await an exception

We use try catch to catch exceptions.

Read the Generator paper carefully to understand why asynchronous exceptions can be caught by try catch at this point.

Because the asynchrony at this point is actually in a scope, with the generator controlling the order of execution, asynchrony can be written as synchronous code, including catch exceptions using try catches.

function fetch(callback) {
    return new Promise((resolve, reject) = > {
        setTimeout((a)= > {
            reject('no')})})}async function main() {
    try {
        const result = await fetch()
        console.log('Request processing', result) // Never execute
    } catch (error) {
        console.log('abnormal', error) / / no
    }
}

main()Copy the code

9 Async Await exception that cannot be caught

As with exceptions that Promise cannot catch in Chapter 5, this is a weakness of await, but it can still be addressed with the solution in Chapter 6:

function thirdFunction() {
    return new Promise((resolve, reject) = > {
        setTimeout((a)= > {
            reject('Let's just converge.')})})}async function main() {
    try {
        const result = await thirdFunction()
        console.log('Request processing', result) // Never execute
    } catch (error) {
        console.log('abnormal', error) // The convergence is abnormal
    }
}

main()Copy the code

Now answer the question at the end of Chapter 6 why await is the more elegant solution:

async function main() {
    try {
        const result1 = await secondFunction() // If no exception is thrown, continue
        const result2 = await thirdFunction() // Throw an exception
        const result3 = await thirdFunction() // Never execute
        console.log('Request processing', result) // Never execute
    } catch (error) {
        console.log('abnormal', error) // The convergence is abnormal
    }
}

main()Copy the code

10 Service Scenarios

Now that the action concept is standard, we can simply converge all exception handling into the action.

Take the following business code as an example. If errors are not caught by default, they bubble up to the top level and then throw an exception.

const successRequest = (a)= > Promise.resolve('a')
const failRequest = (a)= > Promise.reject('b')

class Action {
    async successReuqest() {
        const result = await successRequest()
        console.log('successReuqest'.'Process return value', result) // successReuqest processing returns the value a
    }

    async failReuqest() {
        const result = await failRequest()
        console.log('failReuqest'.'Process return value', result) // Never execute
    }

    async allReuqest() {
        const result1 = await successRequest()
        console.log('allReuqest'.'Process return value SUCCESS', result1) // allReuqest process returns the value SUCCESS a
        const result2 = await failRequest()
        console.log('allReuqest'.'Process return value SUCCESS', result2) // Never execute}}const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()

// The program crashed
// Uncaught (in promise) b
// Uncaught (in promise) bCopy the code

To prevent a program from crashing, the line of business wraps a try catch around all async functions.

We need a mechanism to catch errors at the top level of the action for uniform processing.

In order to supplement the prior knowledge, we once again enter the topic.

One day the Decorator

The Chinese name for a Decorator is a Decorator. The core function of a Decorator is to modify the internal properties of a class directly by wrapping it externally.

Decorators are divided into class decorators method decorators and property decorators based on the location of the decorator.

Class Decorator

Class-level decorator that decorates the entire class and can read and modify any property or method in the class.

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 ascodersCopy the code

Method Decorator

A method-level decorator that decorates a method and performs the same function as a class decorator, but in addition gets the name of the method being decorated.

To exploit this feature, we tamper with the modified function.

const methodDecorator = (target: any, propertyKey: string, descriptor: PropertyDescriptor) = > {
    return {
        get() {
            return (a)= > {
                console.log('classC method override')}}}}class C {
    @methodDecorator
    sayName() {
        console.log('classC ascoders')}}const c = new C()
c.sayName() // classC method overrideCopy the code

Property Decorator

A property-level decorator that modifies an attribute. It performs the same function as a class decorator, but in addition gets the name of the property being modified.

To exploit this feature, we tamper with the values of the decorated property.

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 The ${this.name}`)}}const b = new B()
b.sayName() // classB githubCopy the code

11 Unified exception capture in service scenarios

Let’s write class-level decorators to catch exceptions thrown by async functions:

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
}Copy the code

Wrap all of the class’s methods in a try catch and hand the exception to the business’s unified errorHandler:

const successRequest = (a)= > Promise.resolve('a')
const failRequest = (a)= > Promise.reject('b')

const iAsyncClass = asyncClass(error= > {
    console.log('Unified Exception Handling', error) // unified exception handling b
})

@iAsyncClass
class Action {
    async successReuqest() {
        const result = await successRequest()
        console.log('successReuqest'.'Process return value', result)
    }

    async failReuqest() {
        const result = await failRequest()
        console.log('failReuqest'.'Process return value', result) // Never execute
    }

    async allReuqest() {
        const result1 = await successRequest()
        console.log('allReuqest'.'Process return value SUCCESS', result1)
        const result2 = await failRequest()
        console.log('allReuqest'.'Process return value SUCCESS', result2) // Never execute}}const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()Copy the code

We can also write exception handling at the method level:

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
        }
    }
}Copy the code

The business side is used similarly, except that the decorator needs to be placed on the function:

const successRequest = (a)= > Promise.resolve('a')
const failRequest = (a)= > Promise.reject('b')

const asyncAction = asyncMethod(error= > {
    console.log('Unified Exception Handling', error) // unified exception handling b
})

class Action {
    @asyncAction async successReuqest() {
        const result = await successRequest()
        console.log('successReuqest'.'Process return value', result)
    }

    @asyncAction async failReuqest() {
        const result = await failRequest()
        console.log('failReuqest'.'Process return value', result) // Never execute
    }

    @asyncAction async allReuqest() {
        const result1 = await successRequest()
        console.log('allReuqest'.'Process return value SUCCESS', result1)
        const result2 = await failRequest()
        console.log('allReuqest'.'Process return value SUCCESS', result2) // Never execute}}const action = new Action()
action.successReuqest()
action.failReuqest()
action.allReuqest()Copy the code

12 Business scenarios Do not have the initiative to worry about

What I want to describe is that in a scenario like Chapter 11, the business side doesn’t have to worry about a crash caused by an exception, because all exceptions are caught uniformly at the top level and may appear as a pop-up box telling the user that the request has failed to send.

The business side also does not need to determine whether there is an exception in the program, and runs around in a panic trying catch, because any exception in the program will immediately terminate the subsequent execution of the function without causing worse results.

Err, result := func(), the first argument is an error message, but the next line of code must start with if error {… } in the beginning, the business code of the entire program is riddled with a huge amount of unnecessary error handling, and most of the time, we have to figure out how to handle these errors.

The nodeJS side can return 500 error pockets and immediately interrupt the subsequent request code, which is equivalent to adding a layer of hidden return behind all the dangerous code.

At the same time, the business side also has the absolute initiative, for example, after login failure, if the account does not exist, then directly jump to the registration page, rather than fool prompt user account does not exist, you can do:

async login(nickname, password) {
    try {
        const user = await userService.login(nickname, password)
        // Jump to the home page. This is not executed after a failed login, so don't worry about users seeing strange jumps
    } catch (error) {
        if (error.no === - 1) {
            // Jump to the login page
        } else {
            throw Error(error) // No matter what other mistakes, continue to kick the ball away}}}Copy the code

supplement

On the Nodejs side, remember to listen for global errors and catch catch:

process.on('uncaughtException', (error: any) => {
    logger.error('uncaughtException', error)
})

process.on('unhandledRejection', (error: any) => {
    logger.error('unhandledRejection', error)
})Copy the code

On the browser side, remember to listen for window global errors to catch them:

window.addEventListener('unhandledrejection', (event: any) => {
    logger.error('unhandledrejection', event)
})
window.addEventListener('onrejectionhandled', (event: any) => {
    logger.error('onrejectionhandled', event)
})Copy the code

If there is a mistake, welcome to be corrected, I github home page: github.com/ascoders hope to make friends with people of insight!