v8rs
v8rs

Reputation: 307

Why sometimes error in async code makes the node.js server crash

I have read in some sites that in express.js 'Any uncaught errors in async code could crash the HTTP server causing DoS'. I have made this example to check it, but I want to know why if the error occurs inside the express callback, the server does not crash but if it happens inside the setTimeout() function the server crashes.

Does not the error happen in async code in both examples, or one of them is not async and I am mistaken? Why an uncaught error in some async code makes the server crash and in other async code doesn't?

var express = require("express");

var app = express();

http: app.get("/e1", (req, res, next) => {
  let p = req.query.p;
  let pn = parseInt(p, 10);

  //If the error happens here the server does not crashes
  let s = pn + y; // y does not exist, so an error occurs

  res.send("hi");
});

http: app.get("/e2", (req, res, next) => {
  let p = req.query.p;
  let pn = parseInt(p, 10);

  setTimeout(() => {
    //If the error happens here the server crashes
    let s = pn + y; // y does not exist, so an error occurs
  }, 100);

  res.send("hi");
});

app.listen(3000, function() {
  console.log("Example app listening on port 3000!");
});

Upvotes: 5

Views: 2862

Answers (1)

Jonas Wilms
Jonas Wilms

Reputation: 138457

It may become clear if we think of throw and catch operating on the stack:

throw: goes down the stack until it finds a handler, then continues from there.

catch adds an error handler to the stack.

For synchronous code, that could be visualized as:

 // Legend:
 -> function call
 <- function returns

 http.request -> express.handler -> [try] -> your function -> nested call -> Throw!
 <-            <-                   [catch] <-----------------------------------

Now when you start an asynchronous action, a callback will be called back somewhen, and that callback will end up on a new stack:

 // the current stack:
 http.request -> express.handler -> [try] -> your function -> start async action
 <-            <-                 <-       <-             <-

 // somewhen later, the timeout calls back
 timer -> your setTimeout callback -> nested call -> Throw!
 Crash! <-----------------------------------------------------

Now as Express attaches a catch handler to the callback into your code:

 Express.get = function(callback) {
   //...
   try {
     callback(req, res, next);
   } catch(error) {
    // handle error
   }
 };

that will handle errors, but just synchronous ones (it's simplified, the actual code is here)


Now what should one do to handle that?

Basically: Wrap every callback into a promise (as that makes asynchronous error handling easier):

 const delay = ms => new Promise(res => setTimeout(res, ms));

then await every promise you create, and wrap all that in a try / catch:

 app.get(async (req, res) => {
   try {
      await delay(2000);
      const p = q + d;
   } catch(error) {
     res.status(500).send("whoops");
  }
});

That works because await "keeps the stack" (but just the one of async functions that await on nested calls, thats why we need to add our own try / catch as Express does not await on the callback call), therefore an error inside of one of the nested await ed functions will fall back to our error handler.

 http.request -> express.handler -> [async] -> [try] -> nested call -> [await]
 <-                     <-               <-
 // synchronous code returned, the [async] stack will be kept
 [async] -> [try] -> nested call -> [await]

// somewhen, the promise resolves, execution coninues:
[async] -> [try] -> nested call -> Throw!
<-       <- [catch]  <--------------

Upvotes: 2

Related Questions