Build from scratch Node.js Enterprise web server (4): exception handling

Time:2021-2-27

Exception types and handling methods

Node.js According to the occurrence mode, the anomalies in are divided into synchronous anomalies and asynchronous anomalies, and the latter is further divided into thunk anomalies and promise anomalies

  • Synchronization exceptionIs the exception thrown in the process of synchronous execution, such asthrow new Error();
  • Thunk exceptionAn exception that occurs in an asynchronous callback, such asfs.readFileRead the nonexistent file to call back the first parameter.
  • Promise exceptionIt meansrejectCaused orasyncThe exception thrown in the method can be passed through thecatchMethod capture.

At the end of this paper Node.js editionv12.8.2In, the unhandled synchronization exception will directly cause the process to shut down, the unhandled chunk exception will be ignored, but if it is thrown in the callback, it will cause the process to shut down, and the unhandled promise exception will cause the process warning event, but it will not cause the process to shut down.

In a 7 x 24-hour enterprise web server cluster, multi-layer measures are usually needed to ensure high availability, and program exceptions should be handled at least in the following three layers:

  • Code level exception handling: use programming statements and runtime mechanism to deal with the exception.
  • Process level exception handling: manage abnormal process according to process state and restart policy.
  • Node level exception handling: through load balancing and container choreography and other operation and maintenance means, access to the abnormal node will be transferred away.

This chapter will be based on the work done in the previous chapterhost1-tech/nodejs-server-examples – 03-middlewareCombined with the above three aspects, the code is adjusted.

Add exception handling mechanism

Now write the interface that injects the exception to provide the primaryChaos Engineeringentrance:

// src/controllers/chaos.js
const { Router } = require('express');

const ASYNC_MS = 800;

class ChaosController {
  async init() {
    const router = Router();
    router.get('/sync-error-handle', this.getSyncErrorHandle);
    router.get('/sync-error-throw', this.getSyncErrorThrow);
    router.get('/thunk-error-handle', this.getThunkErrorHandle);
    router.get('/thunk-error-throw', this.getThunkErrorThrow);
    router.get('/promise-error-handle', this.getPromiseErrorHandle);
    router.get('/promise-error-throw', this.getPromiseErrorThrow);
    return router;
  }

  getSyncErrorHandle = (req, res, next) => {
    next(new Error('Chaos test - sync error handle'));
  };

  getSyncErrorThrow = () => {
    throw new Error('Chaos test - sync error throw');
  };

  getThunkErrorHandle = (req, res, next) => {
    setTimeout(() => {
      next(new Error('Chaos test - thunk error handle'));
    }, ASYNC_MS);
  };

  getThunkErrorThrow = () => {
    setTimeout(() => {
      throw new Error('Chaos test - thunk error throw');
    }, ASYNC_MS);
  };

  getPromiseErrorHandle = async (req, res, next) => {
    await new Promise((r) => setTimeout(r, ASYNC_MS));
    next(new Error('Chaos test - promise error handle'));
  };

  getPromiseErrorThrow = async (req, res, next) => {
    await new Promise((r) => setTimeout(r, ASYNC_MS));
    throw new Error('Chaos test - promise error throw');
  };
}

module.exports = async () => {
  const c = new ChaosController();
  return await c.init();
};
// src/controllers/index.js
const { Router } = require('express');
const shopController = require('./shop');
+const chaosController = require('./chaos');

module.exports = async function initControllers() {
  const router = Router();
  router.use('/api/shop', await shopController());
+  router.use('/api/chaos', await chaosController());
  return router;
};

Express provides the default exception handling logic, which will automatically capture the exception and give it to the userfinalhandlerProcessing (output exception information directly). Express can automatically capture synchronization exception and pass thenextCallbacks catch asynchronous exceptions, but cannot catch exceptions thrown directly in asynchronous methods. Therefore, accessing the above interface will have the following effects:

URL effect
http://localhost:9000/api/chaos/sync-error-handle Exceptions are caught and handled
http://localhost:9000/api/chaos/sync-error-throw Exceptions are caught and handled
http://localhost:9000/api/chaos/thunk-error-handle Exceptions are caught and handled
http://localhost:9000/api/chaos/thunk-error-throw Causes the process to shut down abnormally
http://localhost:9000/api/chaos/promise-error-handle Exceptions are caught and handled
http://localhost:9000/api/chaos/promise-error-throw Causes a process warning event

It should be noted that the exception injected by promise error throw has not been caught or caused the process to close abnormally, which will make the program enter a very fuzzy state and bury a high degree of uncertainty in the whole web service

$MKDIR Src / utils # new Src / utils directory to store help tools

$ tree -L 2 -I node_ Modules # display except node_ Directory content structure outside modules
.
├── Dockerfile
├── package.json
├── public
│   ├── glue.js
│   ├── index.css
│   ├── index.html
│   └── index.js
├── src
│   ├── controllers
│   ├── middlewares
│   ├── moulds
│   ├── server.js
│   ├── services
│   └── utils
└── yarn.lock
// src/utils/cc.js
module.exports = function callbackCatch(callback) {
  return async (req, res, next) => {
    try {
      await callback(req, res, next);
    } catch (e) {
      next(e);
    }
  };
};
// src/server.js
// ...
async function bootstrap() {
  // ...
}

+//Listen for uncapped promise exceptions,
+//Exit the process directly
+process.on('unhandledRejection', (err) => {
+  console.error(err);
+  process.exit(1);
+});
+
bootstrap();
// src/controllers/chaos.js
const { Router } = require('express');
+const cc = require('../utils/cc');

const ASYNC_MS = 800;

class ChaosController {
  async init() {
    const router = Router();
    router.get('/sync-error-handle', this.getSyncErrorHandle);
    router.get('/sync-error-throw', this.getSyncErrorThrow);
    router.get('/thunk-error-handle', this.getThunkErrorHandle);
    router.get('/thunk-error-throw', this.getThunkErrorThrow);
    router.get('/promise-error-handle', this.getPromiseErrorHandle);
    router.get('/promise-error-throw', this.getPromiseErrorThrow);
+    router.get(
+      '/promise-error-throw-with-catch',
+      this.getPromiseErrorThrowWithCatch
+    );
    return router;
  }

  // ...

  getPromiseErrorThrow = async (req, res, next) => {
    await new Promise((r) => setTimeout(r, ASYNC_MS));
    throw new Error('Chaos test - promise error throw');
  };
+
+  getPromiseErrorThrowWithCatch = cc(async (req, res, next) => {
+    await new Promise((r) => setTimeout(r, ASYNC_MS));
+    throw new Error('Chaos test - promise error throw with catch');
+  });
}

module.exports = async () => {
  const c = new ChaosController();
  return await c.init();
};

Open the exception injection interface to see the effect:

URL effect
http://localhost:9000/api/chaos/promise-error-throw Causes the process to shut down abnormally
http://localhost:9000/api/chaos/promise-error-throw-with-catch Exceptions are caught and handled

Now the state of the program is very controllable. Next, build the image and start the container with the restart strategy

$# build a container image, named 04 exception and labeled 1.0.0
$ docker build -t 04-exception:1.0.0 .
# ...
Successfully tagged 04-exception:1.0.0

$to mirror 04- exception:1.0.0  Run the container, named 04 exception, and restart unconditionally
$ docker run -p 9090:9000 -d --restart always --name 04-exception 04-exception:1.0.0

visithttp://localhost:9090We can see that when the service process is shut down abnormally, it will automatically restart and continue to run in the expected state.

Health status detection

The service process will be unavailable for a short period of time when it is restarted. In the actual production environment, load balancing will be used to distribute access to multiple application nodes to improve availability. We need to provide health detection to help load balancing determine the direction of traffic. Because the current exception handling mechanism will maintain the reasonable state of the program, as long as an accessible interface is provided, it can represent the health state

// src/controllers/health.js
const { Router } = require('express');

class HealthController {
  async init() {
    const router = Router();
    router.get('/', this.get);
    return router;
  }

  get = (req, res) => {
    res.send({});
  };
}

module.exports = async () => {
  const c = new HealthController();
  return await c.init();
};
// src/controllers/index.js
const { Router } = require('express');
const shopController = require('./shop');
const chaosController = require('./chaos');
+const healthController = require('./health');

module.exports = async function initControllers() {
  const router = Router();
  router.use('/api/shop', await shopController());
  router.use('/api/chaos', await chaosController());
+  router.use('/api/health', await healthController());
  return router;
};

In the subsequent production environment deployment, according to the/api/healthConfigure load balancing to detect the health status of application nodes.

Add more exception handling

Next, replace the express default exception logic with exception page redirection, and add promise exception capture for the store management interface

<!-- public/500.html -->
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <h1>The system is busy. Please try again later</h1>
    Back to the home page</a>
  </body>
</html>
// src/server.js
// ...
async function bootstrap() {
  server.use(express.static(publicDir));
  server.use('/moulds', express.static(mouldsDir));
  server.use(await initMiddlewares());
  server.use(await initControllers());
+  server.use(errorHandler);
  await promisify(server.listen.bind(server, port))();
  console.log(`> Started on port ${port}`);
}

// ...

+function errorHandler(err, req, res, next) {
+  if (res.headersSent) {
+// if an exception occurs when the response result is returned,
+// then give it to the built-in finalhandler of express to close the link
+    return next(err);
+  }
+
+// printing exception
+  console.error(err);
+// redirect to the exception guidance page
+  res.redirect('/500.html');
+}
+
bootstrap();
// src/controllers/shop.js
const { Router } = require('express');
const bodyParser = require('body-parser');
const shopService = require('../services/shop');
const { createShopFormSchema } = require('../moulds/ShopForm');
+const cc = require('../utils/cc');

class ShopController {
  shopService;

  async init() {
    this.shopService = await shopService();

    const router = Router();
    router.get('/', this.getAll);
    router.get('/:shopId', this.getOne);
    router.put('/:shopId', this.put);
    router.delete('/:shopId', this.delete);
    router.post('/', bodyParser.urlencoded({ extended: false }), this.post);
    return router;
  }

-  getAll = async (req, res) => {
+  getAll = cc(async (req, res) => {
    // ...
-  }
+  });

-  getOne = async (req, res) => {
+  getOne = cc(async (req, res) => {
    // ...
-  };
+  });

-  put = async (req, res) => {
+  put = cc(async (req, res) => {
    // ...
-  };
+  });

-  delete = async (req, res) => {
+  delete = cc(async (req, res) => {
    // ...
-  };
+  });

-  post = async (req, res) => {
+  post = cc(async (req, res) => {
    // ...
-  };
+  });
}

module.exports = async () => {
  const c = new ShopController();
  return await c.init();
};

In this way, complete exception handling is done.

Source code of this chapter

host1-tech/nodejs-server-examples – 04-exception

Read more

Build from scratch Node.js Enterprise web server (zero): static services
Build from scratch Node.js Enterprise web server (1): interface and layering
Build from scratch Node.js Enterprise web server (2): Verification
Build from scratch Node.js Enterprise web server (3): Middleware
Build from scratch Node.js Enterprise web server (4): exception handling
Build from scratch Node.js Enterprise web server (5): database access
Build from scratch Node.js Enterprise web server (6): session
Build from scratch Node.js Enterprise web server (7): authentication login
Build from scratch Node.js Enterprise web server (8): network security
Build from scratch Node.js Enterprise web server (9): configuration items
Build from scratch Node.js Enterprise web server (x): log
Build from scratch Node.js Enterprise web server (11): timed tasks
Build from scratch Node.js Enterprise web server (12): remote call
Build from scratch Node.js Enterprise web server (XIII): breakpoint debugging and performance analysis
Build from scratch Node.js Enterprise web server (14): automated testing
Build from scratch Node.js Enterprise web server (XV): summary and Prospect