As A Node.js Novice, I Don't Understand Why Uncaught Exceptions Are So Dangerous

Datetime:2017-04-20 06:11:42         Topic: Node.js          Share        Original >>
Here to See The Original Article!!!

I've been digging into Node.js and Express.js a lot lately and one thing that I keep coming across is the general FUD (Fear, Uncertainty, Doubt) around uncaught exceptions. Specifically around what you should do in the process.on("uncaughtException") event handler in your application. As someone who is relative new to Node.js, I have trouble understanding this fear. Or rather, I don't understand why an uncaught exception is qualitatively more dangerous than a caught exception. Or, how a caught exception is more likely to leave my Node.js application in a known state.

From what I've read in many articles, the common thinking on uncaught exceptions in Node.js is that if they happen, you should just log them and then kill the process. The reasoning behind this perspective is that an uncaught exception leaves your application in an unknown state; and, that it would be dangerous to let any pending requests finish or to accept any new requests.

NOTE: There seems to be some wiggle-room on the "pending requests". Some articles say you should try to gracefully shutdown the process, giving pending requests time to complete. Other articles say that doing that is still dangerous and that all pending requests should be terminated immediately with the process (just giving the logs time to flush).

Now, I'm not trying to argue that uncaught exceptions are good - they aren't. Your application is breaking in a way that you either didn't anticipate or you didn't code for properly. My confusion around uncaught exceptions relates to their relative severity when compared to caught exceptions. I am not sure how caught exceptions have better guarantees.

To help me think this out, I've created a trivial Express.js application that throws three types of errors:

  • Synchronous error inside the Express.js handler.
  • Asynchronous error inside a Promise.
  • Asynchronous error inside a Timer (ie, outside both the handler and the Promise).

There is no business logic behind any of these errors - they are being explicitly thrown in the "service layer". The point here is that the underlying business logic is a black-box to the Express.js application, so the details around the error should be irrelevant.

With the first two errors, I still have access to the current Request / Response model. As such, the contextual responses can be finalized in error. With the last error - the one in the Timer - the subsequent uncaught exception handler won't have access to the response and would otherwise leave the request hanging. As such, I've added some Express.js middleware that terminates long-running requests.

NOTE: Terminating long-running requests is a concept unrelated to uncaught exceptions. There are other reasons why you might want to enforce a request timeout.

For the sake of this thought experiment, I am making all three of the errors the same, "Cannot call foo on undefined". The point here is that a given error can happen anywhere and the business logic is a complete black-box from the point of the Express.js handlers. It's not like only certain types of errors lead to uncaught exceptions.

  • // Require the core node modules.
  • var chalk = require( "chalk" );
  • var express = require( "express" );
  • var http = require( "http" );
  • var onFinished = require( "on-finished" );
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • var app = module.exports = express();
  •  
  • // I setup a long-running request middleware that will terminate requests that do not
  • // appear to have finished successfully.
  • app.use(
  • function( request, response, next ) {
  •  
  • console.log( chalk.green.bold( "GET" ), chalk.green( request.url ) );
  •  
  • var timer = setTimeout(
  • () => {
  •  
  • console.log( chalk.red.italic( "Terminating long-running request." ) );
  • response.status( 504 ).end();
  •  
  • },
  • 5000
  • );
  •  
  • onFinished(
  • response,
  • () => {
  •  
  • clearTimeout( timer );
  •  
  • );
  •  
  • next();
  •  
  • );
  •  
  • // I demonstrate a successful request handler.
  • app.get(
  • "/",
  • function( request, response ) {
  •  
  • response.send( "Hello world!" );
  •  
  • );
  •  
  • // I demonstrate a request handler that will result in a caught error.
  • app.get(
  • "/error1",
  • function( request, response ) {
  •  
  • someSyncCode();
  • response.send( "Error 1 completed." );
  •  
  • );
  •  
  • // I demonstrate a request handler that will result in a caught error.
  • app.get(
  • "/error2",
  • function( request, response, next ) {
  •  
  • someAsyncCode()
  • .then(
  • () => {
  •  
  • response.send( "Error 2 completed." );
  •  
  • .catch( next )
  •  
  • );
  •  
  • // I demonstrate a request handler that will result in a UNCAUGHT error.
  • app.get(
  • "/error3",
  • function( request, response, next ) {
  •  
  • someDangerousAsyncCode()
  • .then(
  • () => {
  •  
  • response.send( "Error 3 completed." );
  •  
  • .catch( next )
  •  
  • );
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • // I throw an error in the same tick of the event loop.
  • function someSyncCode() {
  •  
  • throw( new Error( "Cannot call foo on undefined (1)." ) );
  •  
  •  
  •  
  • // I throw an error inside the returned promise.
  • function someAsyncCode() {
  •  
  • var promise = new Promise(
  • ( resolve, reject ) => {
  •  
  • throw( new Error( "Cannot call foo on undefined (2)." ) );
  •  
  • );
  •  
  • return( promise );
  •  
  •  
  • // I throw an error in the future, outside the bounds of the returned promise.
  • function someDangerousAsyncCode() {
  •  
  • var promise = new Promise(
  • ( resolve, reject ) => {
  •  
  • setImmediate(
  • () => {
  •  
  • throw( new Error( "Cannot call foo on undefined (3)." ) );
  •  
  • );
  •  
  • );
  •  
  • return( promise );
  •  
  •  
  • // ----------------------------------------------------------------------------------- //
  • // ----------------------------------------------------------------------------------- //
  •  
  • app.listen( 3000 );
  •  
  • // Listen for uncaught exceptions - these are errors that are thrown outside the
  • // context of the Express.js controllers and other proper async request handling.
  • process.on(
  • "uncaughtException",
  • function handleError( error ) {
  •  
  • console.log( chalk.red.bold( "Uncaught Exception" ) );
  • console.error( error );
  •  
  • );

Now, if I run this Express.js application and try to call each one of the end-points in turn, I get the following terminal output:

The application works just as we expected it to - the first two errors were caught by the application code and the last error was caught by the process "uncaughtException" handler. So my question is, given the fact that the actual Error was same in all three cases and all of these represent a bug in the application regardless of the context, why does the Node.js community generally believe that the first two leave the application in a "known state" and the last one almost certainly leaves the application in a "dangerous unknown state"?

This is not a retorical question- I am honestly trying to understand the finer aspects of Node.js application development. I feel like I am missing something that is more obvious to seasoned Node.js developers. I keep looking at this code and I cannot figure out why the caught errors are any less dangerous than the uncaught errors?

I think part of my confusion may come from my ColdFusion programming background. In ColdFusion, uncaught exceptions can happen at any time and the ColdFusion application server just keeps on going, regardless of how you may have left the state of the application. And in the vast majority of cases, that's totally fine. But, of course, ColdFusion does a tremendous amount of work for you. I know that Node.js is a lot "closer to the metal"; so, maybe that's a big part of the disconnect for me; or rather, the unfamiliarity with how uncaught exceptions are extra dangerous.

Uncaught exceptions are bad. I'm not, in any way, pushing back against that concept. I'm just not sure why caught exceptions are any better or why they are more likely to leave your application in a safe, known state. If anyone can shed some light on this, I would be hugely grateful.

Tweet This Groovy post by @BenNadel - As A Node.js Novice, I Don't Understand Why Uncaught Exceptions Are So Dangerous Thanks my man — you rock the party that rocks the body!








New