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