preface

In development, do you frequently write try/catch logic in async functions for system robustness or to catch asynchronous errors?

async function func() {
    try {
        let res = await asyncFunc()
    } catch (e) {
      / /...}}Copy the code

I once mentioned an elegant way to handle async/await in 28 JavaScript Tricks a Qualified Intermediate Front End Engineer must Master

We can then wrap the async function with a helper function to implement error catching

async function func() {
    let [err, res] = await errorCaptured(asyncFunc)
    if (err) {
        / /... Error trapping
    }
    / /...
}
Copy the code

One drawback is that you have to introduce the errorCaptured helper every time you use it. Is there a lazy way to do that?

The answer is definitely yes. A new idea I proposed after that blog post is to use a WebPack Loader to automatically inject try/catch code. Hopefully, this will be the end result

// development
async function func() {
   let res = await asyncFunc()
    / /... Other logic
}

// release
async function func() {
    try {
        let res = await asyncFunc()
    } catch (e) {
      / /...
    }
    / /... Other logic
}
Copy the code

Isn’t that great? Without any extra code in the development environment, let webPack automatically inject error-catching logic into the production code, and let’s implement this loader step by step

Loader principle

We define a loader in Webpack, which is essentially a function. When we define a loader, we also define a test property. Webpack iterates through all the module names and, when the re defined by the test attribute is matched, passes the module as the source parameter to the Loader for execution

{
    test: /\.vue$/.use: "vue-loader",}Copy the code

The use attribute can be followed by a string or a path. If the use attribute is a string, the nodeJS module will be treated as node_modules by default

These files are essentially strings (images and videos are Buffer objects). For example, when the Vue-Loader receives a file, it divides it into three parts by matching the string. The template string is compiled by vue-Loader to render. The script part is given to Babel-Loader, and the style part is given to CSS-Loader. Meanwhile, loaders comply with the single principle, that is, one loader does only one thing, so that multiple Loaders can be flexibly combined without interfering with each other

Implementation approach

Because loaders can read matched files and process them into the desired output, we can implement our own loader that accepts JS files and wraps the code with a try/catch layer when we encounter the await keyword

How exactly can I wrap the try/catch to the await and subsequent expressions? Knowledge of abstract Syntax trees (AST) is needed here

AST

Abstract syntax tree is an abstract representation of source code syntax structure. It represents the syntactic structure of a programming language in the form of a tree, with each node in the tree representing a structure in the source code

There are a number of very useful functions that can be implemented with AST, such as converting code from ES6 to ES5, esLint checking, code beautification, and even js engines that rely on AST. Also, since the code is purely string in nature, it is not limited to js conversion. SCSS, CSS preprocessors such as Less also use THE AST to convert CSS code to browsers. Let’s take an example

let a = 1
let b = a + 5
Copy the code

This is what happens when you convert it to an abstract syntax tree

Turning a string into an AST tree requires both lexical analysis and syntax analysis

Lexical analysis converts individual snippets into tokens (lexical units), removing whitespace comments. For example, the first line converts let, a, =, and 1 into tokens. A token is an object that describes the location of the snippet in the entire code and records some information about its current value

Syntax analysis converts the token into Node in combination with the syntax of the current language (JS), and Node contains a type attribute to record the current type. For example, let in JS represents the keyword of a variable declaration. So its type is VariableDeclaration, and a = 1 is described as a let declarator, and the declaration is dependent on the VariableDeclaration, so it’s kind of hierarchical

In addition, you can see that there is not a token for a Node. To make a declaration, you must have values on both sides of the equal sign, otherwise you will be warned. This is the basic principle of ESLint. Finally, all nodes are combined to form an AST syntax tree

Recommend a very useful AST viewing tool,AST explorerFor a more intuitive view, how does the code turn into an abstract syntax tree

Going back to the implementation of the code, we simply need to find the await expression through the AST tree and wrap the await around a try/catch Node

async function func() {
   await asyncFunc()
}
Copy the code

Corresponding to the AST tree:

async function func() {
    try {
        await asyncFunc()
    } catch (e) {
        console.log(e)
    }
}
Copy the code

Corresponding to the AST tree:

Loader development

When our loader receives the source file, we can use the @babel/ Parser package to convert the file into an AST abstract syntax tree. How do we find the corresponding await expression?

This requires another Babel package, @babel/traverse, which passes an AST tree and some hook functions, and then traverses the passed AST tree in depth, executing the corresponding callback if the node is the same name as the hook function

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default

module.exports = function (source) {
    let ast = parser.parse(source)

    traverse(ast, {
        AwaitExpression(path) {
            / /...}})/ /...
}
Copy the code

Using @babel/traverse we can easily find the Node corresponding to the await expression. The next step is to create a Node of type TryStatement and put the await inside. We also need to rely on @babel/types, which is a Babel version of the loadsh library. It provides a number of helper functions related to AST nodes. We need to use the tryStatement method. Create a TryStatement Node

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

module.exports = function (source) {
    let ast = parser.parse(source)

    traverse(ast, {
        AwaitExpression(path) {
            let tryCatchAst = t.tryStatement(
                / /...
            )
            / /...}})}Copy the code

A tryStatement takes three arguments. The first is a try clause, the second is a catch clause, and the third is a finally clause. A complete try/catch statement for a Node looks like this

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

module.exports = function (source) {
    let ast = parser.parse(source)

    traverse(ast, {
        AwaitExpression(path) {
            let tryCatchAst = t.tryStatement(
                // Try clause (required)
                t.blockStatement([
                    t.expressionStatement(path.node)
                ]),
                / / catch clause
                t.catchClause(
                    / /...
                )
            )
            path.replaceWithMultiple([
                tryCatchAst
            ])
        }
    })
      / /...
}

Copy the code

Use the blockStatement, expressionStatement methods to create a block-level Node with an await expression. @babel/traverse passes a path to each hook function. Contains information about the current traversal, such as the current Node, the path object of the last traversal, and the corresponding Node. Most importantly, there are some methods to operate on the Node. We need to use the replaceWithMultiple method to replace the current Node with a try/catch Node

We also need to consider that the await expression may be treated as a declaration statement

 let res = await asyncFunc()
Copy the code

It could also be an assignment statement

 res = await asyncFunc()
Copy the code

Or maybe it’s just a simple expression

 await asyncFunc()
Copy the code

The AwaitExpression hook function is used to determine what type of Node the parent Node is. The AwaitExpression hook function is used to determine what type of Node the parent Node is. The AwaitExpression hook function is used to determine what type of Node the parent Node is. You can also use the AST Explorer to view the structure of the FINAL AST tree that needs to be generated

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")

module.exports = function (source) {
    let ast = parser.parse(source)

    traverse(ast, {
        AwaitExpression(path) {
            if (t.isVariableDeclarator(path.parent)) { // Declare variables
                let variableDeclarationPath = path.parentPath.parentPath
                let tryCatchAst = t.tryStatement(
                    t.blockStatement([
                        variableDeclarationPath.node // Ast
                    ]),
                    t.catchClause(
                        / /...
                    )
                )
                variableDeclarationPath.replaceWithMultiple([
                    tryCatchAst
                ])
            } else if (t.isAssignmentExpression(path.parent)) { // Assign an expression
                let expressionStatementPath = path.parentPath.parentPath
                let tryCatchAst = t.tryStatement(
                    t.blockStatement([
                        expressionStatementPath.node
                    ]),
                    t.catchClause(
                        / /...
                    )
                )
                expressionStatementPath.replaceWithMultiple([
                    tryCatchAst
                ])
            } else { // await expression
                let tryCatchAst = t.tryStatement(
                    t.blockStatement([
                        t.expressionStatement(path.node)
                    ]),
                    t.catchClause(
                        / /...
                    )
                )
                path.replaceWithMultiple([
                    tryCatchAst
                ])
            }
        }
    })
      / /...
}
Copy the code

After you get the replaced AST tree, use the @babel/core package transformFromAstSync method to convert the AST tree back to the corresponding code string

const parser = require("@babel/parser")
const traverse = require("@babel/traverse").default
const t = require("@babel/types")
const core = require("@babel/core")

module.exports = function (source) {
    let ast = parser.parse(source)

    traverse(ast, {
        AwaitExpression(path) {
              / / same as above}})return core.transformFromAstSync(ast).code
}
Copy the code

Some loader configuration items are also exposed to improve ease of use. For example, the await statement is not injected again if it has been wrapped by a try/catch. The principle is also AST based, using the findParent method of the path parameter to traverse all parent nodes. Check whether the Node is wrapped by a try/catch

traverse(ast, {
    AwaitExpression(path) {
        if (path.findParent((path) = > t.isTryStatement(path.node))) return
        // Processing logic}})Copy the code

In addition, the code snippet in the catch clause can be customized so that all errors are handled using unified logic. The principle is to convert the user-configured code snippet into an AST that is passed as an argument to the catch node when the TryStatement node is created

Further improvement

After talking in the comment section, I changed the default to add a try/catch to each await statement to wrap a try/catch around the entire async function by first finding the await statement and then recursively iterating up

When async is found, create a try/catch Node Node and replace the body of the async function with the child Node of the original async function

When a try/catch is encountered, it indicates that it has been wrapped by a try/catch, uninject, and exit the traversal directly. In this way, if the user has a custom error catching code, the default catching logic of the loader will not be executed

Corresponding to the AST tree:

Corresponding to the AST tree:

This is just the basic async function declaration of a node node. There are other representations of function expressions, arrow functions, methods as objects, and so on. When one of these cases is met, a try/catch block is injected

// Function expression
const func = async function () {
    await asyncFunc()
}

// Arrow function
const func2 = async() = > {await asyncFunc()
}

/ / method
const vueComponent = {
    methods: {
        async func() {
            await asyncFunc()
        }
    }
}
Copy the code

conclusion

This article is intended to introduce some useful examples. In the daily development process, you can develop a loader that is more suitable for your own business line. For example, the technology stack is an old project of jQuery, the Node Node can match $

I’m sorry. You know how to compile. You can really do whatever you want

By developing this loader, you can not only learn how webPack Loader works, but also learn a lot about AST and how Babel works. For more methods, you can check the official documents of Babel or the script of Babel

I have published this loader to NPM, interested friends can directly call NPM install async-catch-loader -d installation and research, the use method is the same as the general loader, remember to put it after babel-Loader, In order to execute first, the result of the injection continues to be given to the Babel escape

{
    test: /\.js$/.use: [
        "babel-loader? cacheDirectory=true".'async-catch-loader']}Copy the code

More details and source code can be viewed on Github. In the meantime, if you have anything to gain from this article, please click a star. Thank you very much

async-catch-loader