One, foreword

You probably know that KOA was written by the original Express core team, so why would they want to recreate a KOA wheel after Express? I’m going to give you some analysis today. Hope to play a role in the introduction of jade.

What are the disadvantages of Express? What issues with Express did KOA solve? This is also asked in some interview questions. So, in order to realize their ideals (money), like-minded comrades can follow me to analyze.

I want to start with a very important feature of Express, which is middleware. Middleware is used throughout Express. We often use application-level middleware in Express, such as:

    const app = require('express') (); app.use((req, res, next) = > {
        // Do something...
        next();
    })

Copy the code

Another example is the more commonly used routing level middleware. Why do I call it routing level? Because it also maintains a next inside

    app.get('/'.(req, res, next) = > {
        res.send('something content');
    })

Copy the code

I won’t go into the details of the middleware here. I have a detailed analysis of the middleware behind, welcome everyone to watch.

So as we can see, there’s a key next in there, and what it does inside Express is get the key to the next middleware from the stack.

So here’s the thing: we started looking at what the implementation of Express was hiding.

Second, middleware problem analysis

Here’s an example:

    const Express = require('express');
    const app = new Express();
    const sleep = () = > new Promise(resolve= > setTimeout(function(){resolve(1)}, 2000));
    const port = 8210;
    function f1(req, res, next) {
      console.log('this is function f1.... ');
      next();
      console.log('f1 fn executed done');
    }
    
    function f2(req, res, next) {
      console.log('this is function f2.... ');
      next();
      console.log('f2 fn executed done');
    }
    
    async function f3(req, res) {
      console.log('f3 send to client');
      res.send('Send To Client Done');
    }
    app.use(f1);
    app.use(f2);
    app.use(f3);
    app.get('/', f3)
    app.listen(port, () = > console.log(`Example app listening on port ${port}! `))
    

Copy the code

Ideal return, and true return, are no problem at present.

    this is function f1.this is function f2.f3 send to client
    f1 fn executed done
    f2 fn executed done
    
Copy the code

Ok, so let’s move on to the next example. In the next example, everything else is unchanged except for one place:

    const sleep = () = > new Promise(resolve= > setTimeout(function(){resolve()}, 1000))
    async function f3(req, res) {
        await sleep();
      console.log('f3 send to client');
      res.send('Send To Client Done');
    }
Copy the code

What order do you think the return values are in?

We might think that there is no change because we are adding await, so we should wait for the await to finish before executing the code below. Well, it didn’t. The result returned is:

    this is function f1.this is function f2.f1 fn executed done
    f2 fn executed done
    f3 send to client

Copy the code

What happened? You might be a little surprised. However, if you dig deep into the Express source code, the cause of the problem becomes obvious.

Specific source code I in this article will not be detailed analysis, directly say the conclusion:

Middleware calls in Express are not promises so even if we add async await it doesn’t work.

So how is it used in KOA?

const Koa = require('koa');
const app = new Koa();
const sleep = () = > new Promise(resolve= > setTimeout(function(){resolve()}, 1000))
app.use(async (ctx, next) => {
    console.log('middleware 1 start');
    await next();
    console.log('middleware 1 end');
});
app.use(async (ctx, next) => {
    await sleep();
    console.log('middleware 2 start');
    await next();
    console.log('middleware 2 end');
});

app.use(async (ctx, next) => {
    console.log('middleware 3 start')
    ctx.body = 'test middleware executed';
})


Copy the code

As expected, the order of implementation is:

middleware 1 start
middleware 2 start
middleware 3 start
middleware 2 end
middleware 1 end


Copy the code

The reason: KOA uses promises internally, so it can control the execution of the order.

From the above examples, we know that when using express middleware, if you don’t know how it works, it’s easy to get stuck. Koa implements the Onion model by using async and await next(), i.e., next to the next middleware, and as soon as the middleware below completes, the middleware above is executed layer by layer again until all is complete.

3. Error logic capture

3.1 Express error capture logic

Again, let’s look at express’s ability to catch error logic:

app.use((req, res, next) = > {
    // c is not defined
    const a = c;
});

// Error handling middleware
app.use((err, req, res, next) = > {
    if(error) {
        console.log(err.message);
    }
    next()
})

process.on("uncaughtException".(err) = > {
    console.log("uncaughtException message is::", err);
})

Copy the code

Let’s look at another asynchronous processing:

app.use((req, res, next) = > {
    // c is not defined
    try {
        setTimeout(() = > {
            const a = c;
            next()
        }, 0)}catch(e) {
        console.log('Asynchronous error, can you catch it? ')}}); app.use((err, req, res, next) = > {
    if(error) {
        console.log('Will it be executed here? ', err.message);
    }
    next()
})


process.on("uncaughtException".(err) = > {
    console.log("uncaughtException message is::", err);
})

Copy the code

Can you guess the difference between synchronous and asynchronous?

The answer is: a lot!!

To be specific,

  • No trigger is triggered during synchronizationuncaughtException“Into the error-handling middleware.
  • Asynchronously,Don'tTrigger error handling middleware,And willThe triggeruncaughtException

What happened in between?

3-1. Low-level logic obtained by synchronization logic errors

The logic is that express internally blocks synchronized errors, so it does not pass to uncaughtException. If an error occurs, it goes directly to the other middleware. UncaughtException is not entered in node’s uncaughtException, even if there is no error handler. In this case, 500 errors are reported.

3-2 The underlying logic obtained by the asynchronous logic error

Again, because express’s implementation does not take Promise into account, its middleware execution is sequential and synchronous. So if you have asynchronous, the error-handling middleware is practically unstoppable, so Express has nothing to do with asynchronous processing errors in this middleware.

From the asynchronous firing example above, except for the error-handling middleware, the try catch among us did not fire either. This is a pit that everyone might step on. It’s all about how javascript works. See # async queue try catch problem for specific reasons

So to catch the current error, you need to use async await

app.use(async (req, res, next) => {
    try {
        await (() = > new Promise((resolve, reject) = > {
            http.get('http://www.example.com/testapi/123'.res= > {
                reject('The assumption is wrong');
            }).on('error'.(e) = > {
                throw new Error(e); })}) (); }catch(e) {
        console.log('Asynchronous error, can you catch it? ')}});Copy the code

In this way, not only can our catch be obtained, but also uncaughtException can be obtained.

3.2 KOA’s error acquisition logic

In general, it is similar to Express, because the underlying processing of JS is the same. But there are still differences in use.

The Onion model, also mentioned above, is characterized by the middleware that starts at the beginning and finishes at the end, so on KOA you can put the error-handling middleware at the top of the middleware logic.

const http = require('http');
const Koa = require('koa');
const app = new Koa();
app.use(async (ctx, next)=>{
    try {
        await next();
    } catch (error) {
        // Respond to the user
        ctx.status = 500;
        ctx.body = 'Go to default error middleware';
        // ctx.app.emit('error', error); // Triggers an application-level error event}}); app.use(async (ctx, next) => {
    await (() = > new Promise((resolve, reject) = > {
        http.get('http://www.example.com/testapi/123'.res= > {
            reject('The assumption is wrong');
        }).on('error'.(e) = > {
            throw new Error(e); })}) ();await next();
})

Copy the code

The above code, reject, generates error messages that are captured by the uppermost error-handling middleware.

In summary, js underlying mechanism is the same, but the use of methods and details of the point is not the same, we use the time to pay attention to, should be able to quickly master ~~