The introduction of

We all know why try catch fails to catch errors in setTimeout asynchronous tasks. And asynchronous code is particularly common in JS. How can we compare?

A situation that cannot be captured

function main() {
  try {
    setTimeout((a)= > {
      throw new Error('async error')},1000)}catch(e) {
    console.log(e, 'err')
    console.log('continue... ')
  }
}

main();
Copy the code

In this code, the setTimeout callback throws an error that will not be caught in the catch, causing the program to crash.

So try catch in JS doesn’t mean you can just write one and rest easy. Does every function have to be written, so when does a try catch fail to catch an error?

Asynchronous tasks

  • The error in the callback function of the macro task cannot be caught

    Write a try catch in the main task, and then call the asynchronous task, which will throw an error a second later.

    // Asynchronous tasks
    const task = (a)= > {
      setTimeout((a)= > {
       throw new Error('async error')},1000)}/ / the main task
    function main() {
      try {
        task();
      } catch(e) {
        console.log(e, 'err')
        console.log('continue... ')}}Copy the code

    Main cannot catch an error in this case because of the browser’s execution mechanism. An asynchronous task is added to the task queue by eventLoop and pulled out (the js main process) for execution. By the time the task is pulled out, main’s stack has already exited, meaning that the context has changed, so Main cannot catch errors in the Task.

    Event callback, request callback belongs to both Tasks, so the reasoning is the same. Read this article for an EventLoop review

  • A callback to a microtask (promise)

    // Return a Promise object
    const promiseFetch = (a)= > 
      new Promise((reslove) = > {
      reslove();
    })
    
    function main() {
      try {
        // The callback function throws an error
        promiseFetch().then((a)= > {
          throw new Error('err')})}catch(e) {
        console.log(e, 'eeee');
        console.log('continue'); }}Copy the code

    Promise’s task, the callback function inside then, throws an error and does not catch either. Since the microtask queue is emptied between two tasks, when then is pushed, main is already off the stack.

It’s not that callbacks can’t try catch

One misconception that many people have is that callbacks do not catch most of the time.

Not exactly. Look at the most common chestnut.

// Define a fn as a function.
const fn = (cb: () = > void) => {
  cb();
};

function main() {
  try {
    Callback is called by the fn execution and an error is thrown.
    fn((a)= > {
      throw new Error('123'); })}catch(e) {
    console.log('error');
  }
}
main();
Copy the code

The result, of course, is catch. This is because the callback is executed in the same eventloop as main, which is an eventloop tick. So the context doesn’t change and the error is catch-able. The root cause is still synchronous code, and no asynchronous tasks are encountered.

Exception capture for promise

The constructor

Let’s start with two pieces of code:

function main1() {
  try {
    new Promise((a)= > {
      throw new Error('promise1 error')})}catch(e) {
    console.log(e.message); }}function main2() {
  try {
    Promise.reject('promise2 error');
  } catch(e) {
    console.log(e.message); }}Copy the code

Neither of the above try catches will catch an error, because errors inside a promise will not bubble up, but will be eaten by the promise. Only the promise. Catch can be caught, so make sure you write a catch with a promise.

Then let’s look at two pieces of code that use promise.catch:

// reject
const p1 = new Promise((reslove, reject) = > {
  if(1) { reject(); }}); p1.catch((e) = > console.log('p1 error'));
Copy the code
// throw new Error
const p2 = new Promise((reslove, reject) = > {
  if(1) {
    throw new Error('p2 error')}}); p2.catch((e) = > console.log('p2 error'));
Copy the code

Either reject or throw new Error inside a promise can be caught by a catch callback.

This is different from our initial microtask, where the promise microtask refers to the callback to then, and where the promise constructor passes in the first argument, new Promise is executed synchronously.

then

How do you catch errors after then?

function main3() {
  Promise.resolve(true).then((a)= > {
    try {
      throw new Error('then');
    } catch(e) {
      return e;
    }
  }).then(e= > console.log(e.message));
}
Copy the code

An error can only be caught inside a callback and returned, and the error is passed to the next then callback.

Catch asynchronous errors with promises


const p3 = (a)= >  new Promise((reslove, reject) = > {
  setTimeout((a)= > {
    reject('async error'); })});function main3() {
  p3().catch(e= > console.log(e));
}
main3();
Copy the code

Wrap asynchronous operations in Promise, judge them internally, reject errors, and catch them externally with Promise.

Exception catching of async/await

First, we simulate a request failure function called fetchFailure. The fetch function usually returns a promise.

The main function is async, and catch catches errors thrown by fetchFailure Reject. Can you get it?

const fetchFailure = (a)= > new Promise((resolve, reject) = > {
  setTimeout((a)= > {// simulate the request
    if(1) reject('fetch failure... '); })})async function main () {
  try {
    const res = await fetchFailure();
    console.log(res, 'res');
  } catch(e) {
    console.log(e, 'e.message');
  }
}
main();
Copy the code

Async functions are compiled into several sections according to await keyword and catch etc. For example main is split into three sections.

  1. fetchFailure 2. console.log(res) 3. catch

Step is used to control the progress of the iteration, such as “next”, which is used to go down once, from 1->2. Asynchronism is controlled by promise.then (), which is a Promise chain, if you are interested. The key is that the generator also has a “throw” state. When a Promise, reject, bubbles up until step(‘throw’) is executed, and then the code console.log(e, ‘e.message’) in the catch; The execution.

It definitely feels like async/await error handling is a bit more elegant, and of course uses Promise internally.

further

Async functions are useful for processing asynchronous processes, but they do not automatically catch errors, so we need to write our own try catch. If we write one for each function, it is also very troublesome, because there will be many asynchronous functions in comparison services.

The first thing that comes to mind is to extract the try catch and the logic that follows the catch.


const handle = async (fn: any) => {
  try {
    return await fn();
  } catch(e) {
    // do sth
    console.log(e, 'e.messagee'); }}async function main () {
    const res = await handle(fetchFailure);
    console.log(res, 'res');
}
Copy the code

Write a higher-order function wrapped around fetchFailure, which reuses the logic, such as the try catch here, and then executes the argument – function passed in.

Then, we add the arguments passed to the callback function and the return value complies with first-error, similar to the node/go syntax. As follows:

const handleTryCatch = (fn: (... args: any[]) = > Promise= > < {} >)async(... args: any[]) => {try {
    return [null.awaitfn(... args)]; }catch(e) {
    console.log(e, 'e.messagee');
    return[e]; }}async function main () {
  const [err, res] = await handleTryCatch(fetchFailure)(' ');
  if(err) {
    console.log(err, 'err');
    return;
  }
  console.log(res, 'res');
}

Copy the code

However, there are still several problems. One is the logic after catch, which does not support customization. Another is that the return value always needs to be determined whether there is error or not, which can also be abstracted. Therefore, we can do some articles on the catch of higher-order functions, such as adding some error handling callback functions to support different logic, and then the error handling in a project can be simply divided into several classes and do different processing, so that the code can be reused as much as possible.

// 1. Third order function. The first pass is a handle for error handling, the second pass is the async function to decorate, and finally a new function is returned.

const handleTryCatch = (handle: (e: Error) = > void = errorHandle) =>
  (fn: (... args: any[]) = > Promise= > < {} >)async(... args: any[]) => {try {
      return [null.awaitfn(... args)]; }catch(e) {
      return[handle(e)]; }}// 2. Define various error types
// We can format the error message into a style that can be handled in the code, such as including error codes and error messages
class DbError extends Error {
  public errmsg: string;
  public errno: number;
  constructor(msg: string, code: number) {
    super(msg);
    this.errmsg = msg || 'db_error_msg';
    this.errno = code || 20010; }}class ValidatedError extends Error {
  public errmsg: string;
  public errno: number;
  constructor(msg: string, code: number) {
    super(msg);
    this.errmsg = msg || 'validated_error_msg';
    this.errno = code || 20010; }}// 3. Error handling logic, which may be just one category. Error handling is usually broken down by functional requirements
// For example, the request fails (200 but the return value has an error message), for example, the node fails to write db, etc.
const errorHandle = (e: Error) = > {
  // do something
  if(e instanceof ValidatedError || e instanceof DbError) {
    // do sth
    return e;
  }
  return {
    code: 101.errmsg: 'unKnown'
  };
}   
const usualHandleTryCatch = handleTryCatch(errorHandle);

// All of the above code is reused by multiple modules, so the actual business code might just need this.
async function main () {
  const [error, res] = await usualHandleTryCatch(fetchFail)(false);
  if(error) {
    // Since catch already does the interception, you can even add some general logic, not even if error
    console.log(error, 'error');
    return;
  }
  console.log(res, 'res');
}
Copy the code

After solving some error logic reuse problems, that is, packaging into different error handlers. However, when these processors are used, they can be written as es6 decorators because they are higher-order functions.

Decorators, however, can only be used for classes and class methods, so they cannot be used if they are in the form of functions. However, in daily development, such as the React component, or Mobx store, all exist in the form of class, so there are a lot of scenarios.

For example, change to class decorator:

const asyncErrorWrapper = (errorHandler: (e: Error) = > void = errorHandle) => (target: Function) = > {
  const props = Object.getOwnPropertyNames(target.prototype);
  props.forEach((prop) = > {
      var value = target.prototype[prop];
      if(Object.prototype.toString.call(value) === '[object AsyncFunction]'){
        target.prototype[prop] = async(... args: any[]) => {try{
            return await value.apply(this,args);
          }catch(err){
            returnerrorHandler(err); }}}}); } @asyncErrorWrapper(errorHandle)class Store {
  async getList (){
    return Promise.reject('Class decoration: Failed'); }}const store = new Store();

async function main() {
  const o = await store.getList();
}
main();
Copy the code

This class decoration is written by Huang Ziyi, thanks for inspiration.

Koa error handling

If you are not familiar with KOA, you can choose to skip it.

Of course, koA can also use the above async approach, but usually when we use KOA to write server, it is a request, HTTP transaction will drop response middleware, so KOA error handling makes good use of the characteristics of middleware.

For example, my approach is that the first middleware catches errors, and because of the Onion model, the first middleware will still execute, and when one of the middleware throws an error, I expect to catch it and handle it there.

// The first middleware
const errorCatch = async(ctx, next) => {
  try {
    await next();
  } catch(e) {
    // Catch error routes here, throw error
    console.log(e, e.message, 'error');
    ctx.body = 'error';
  }
}

app.use(errorCatch);

// logger
app.use(async (ctx, next) => {
  console.log(ctx.req.body, 'body');
  await next();
})

// Some middleware of the router
router.get('/error'.async (ctx, next) => {
  if(1) {
    throw new Error('Error test')}await next();
})


Copy the code

Why write a try catch on the first middleware and catch the previous middleware throw? First of all, we explained in async/await earlier that await handle() in async and throw new Error or promise.reject () inside handle can be caught by async catch. So all you need is the next function to take the error and throw it, so look at the next function.

// Compose is the array passed into the middleware that eventually forms the middleware chain, and next controls the cursor.
 compose(middlewares) {
    return (context) = > {
      let index = 0;
      // In order for each middleware to be called asynchronously, i.e. written 'await next()', each next should return a promise object

      function next(index) {
        const func = middlewares[index];
        try {
          // We write a try catch here, because we write it in the Promise constructor, so any errors thrown can be caught
          return new Promise((resolve, reject) = > {
            if (index >= middlewares.length) return reject('next is inexistence');
            resolve(func(context, () => next(index + 1)));
          });
        } catch(err) {
          // Error caught, error returned
          return Promise.reject(err); }}returnnext(index); }}Copy the code

The next function fetches the current middleware execution based on index. If the middleware function is async function, it is also converted to generator execution, and the internal asynchronous code sequence is controlled by itself. As we know, the errors of async function can be caught by try catch. So add a try catch to the next function to catch any errors of the middleware function and return them. That’s why we can capture it in the first middleware. The code can be seen in the simplified version of KOA

Then KOA also provides ctx.throw and global app.on to catch errors. If you don’t have error-handling middleware, you can use ctx.throw to return to the front end so that your code doesn’t fail. But throw new Error also has advantages, because in the code logic of one middleware, if we don’t want the middleware to execute it, we can directly return the Error to the front end, and let the generic middleware handle it, it is an Error message anyway.

// Define different error types that can be caught and processed here.
const errorCatch = async(ctx, next) => {
  try {
    await next();
 } catch (err) {
    const { errmsg, errno, status = 500, redirect } = err;
    
    if (err instanceof ValidatedError || err instanceof DbError || err instanceof AuthError || err instanceof RequestError) {
      ctx.status = 200;
      ctx.body = {
        errmsg,
        errno,
      };
      return;
    }
    ctx.status = status;
    if (status === 302 && redirect) {
      console.log(redirect);
      ctx.redirect(redirect);
    }
    if (status === 500) {
      ctx.body = {
        errmsg: err.message,
        errno: 90001}; ctx.app.emit('error', err, ctx);
    }
  }
}

app.use(errorCatch);

// logger
app.use(async (ctx, next) => {
  console.log(ctx.req.body, 'body');
  await next();
})

/ / by CTX. Throw
app.use(async (ctx, next) => {
  //will NOT log the error and will return `Error Message` as the response body with status 400
  ctx.throw(400.'Error Message');
}); 

// Some middleware of the router
router.get('/error'.async (ctx, next) => {
  if(1) {
    throw new Error('Error test')}await next();
})

// The last of the pack
app.on('error', (err, ctx) => {
  /* centralized error handling: * console.log error * write error to log file * save error and request information to database if ctx.request match condition * ... * /
});

Copy the code

The last

This is where the code for this article is stored

In general, it is convenient for async and promise to handle ASYNCHRONOUS errors in JS at present. In addition, mature frameworks (React, KOA) have good ways of handling errors, so try to see how the authorities handle them.

This is just my understanding of how ASYNCHRONOUS errors are handled in JS. However, there are many exceptions that need to be caught in the front end, such as front-end code errors, CORS cross-domain errors, IFrame errors, and even React and VUE errors, which need to be dealt with, as well as the monitoring and reporting of exceptions to help us timely solve problems and analyze stability. Adopt a variety of programs to apply to our project, so that we do not worry about the page hung, or reported a bug, in order to go to vacation and rest 😆

Finally, the blog address: github.com/sunyongjian…