When writing Node.js programs, we often encounter the question: Why does my program never exit?

  • The CLI tool has encountered a problem and is abnormal, but it is still running and does not return a value
  • The Electron program has obviously closed the window, but the command line does not exit
  • The event function on the cloud function/Lambda has failed, but does not finish, and continues to execute the scheduled task until it times out

In Node.js, this problem is most likely related to the type of exception generated and the corresponding error mechanism:

In JavaScript, the use of asynchronous event queues simplifies the handling of concurrent events, but also increases the case and complexity of exceptions. In order to handle high concurrency scenarios and CPU-intensive tasks, Node.js introduces multi-threaded workers, which further increases the abnormal situation. In addition, Node.js also has uncaughtException, unhandledRejection and other events to handle global exception events.

In general, there are many possibilities of exceptions in Node.js: ordinary exceptions, exceptions in promises, exceptions in timers, and exceptions in workers. Exceptions can also be handled in a variety of ways: try-catch statements, global events, Worker events, etc. How to properly catch and handle these corresponding exceptions is an interesting problem. In practice, this is also an important problem: incorrect exception capture may lead to program crash, program crash due to residual timer does not exit, program crash can not locate the root cause and other problems.

Below, let’s classify and analyze the exceptions and their capture mechanism in each scenario:

If you really don’t want to understand the details of the mechanism, and you want your program to exit on exception, then the following two steps will do:

  1. Add the following code at the entry and exit of all threads to handle normal exceptions, Promise exceptions, and timer exceptions, which can be handled centrally here:
process.on('unhandledRejection', (err) => {
	throw err;
})

process.on('uncaughtException', (err) => {
	throw err;
})
Copy the code
  1. Add the following code at all starting workers to throw the top-level exception in the Worker thread in the thread starting the Worker
const worker = new Worker(...)
worker.on('error', (err) => {
	throw err;
});
Copy the code

The specific reasons for doing this and the detailed mechanism involved, let’s start to break it down below:

[TOC]

Ordinary anomalies

The situation here is very plain and simple, just like any other programming language, but things will get more complicated later, so let’s set the stage here.

In simple terms, we directly throw an exception, such an exception will no doubt directly terminate the running of the program, and the still running we check whether the program is still running has no output.

// simple.js
setInterval(() = > {
    console.log('still running');
}, 500)

throw 'simple';

/* throw 'simple'; ^ simple (Use `node --trace-uncaught ... ` to show where the exception was thrown) */
Copy the code

try-catchAnd ordinary anomaly

This is not unusual, but let’s take a look at a try-catch statement with a simple exception: you can see that the program does not exit, but runs correctly, just like in a normal programming language.

setInterval(() = > {
    console.log('still running');
}, 500)


try {
    throw 'simple';
} catch(err) {
    console.log('catch', err);
}


/*
catch simple
still running
still running
still running
still running
*/
Copy the code

process.on("uncaughtException")And ordinary anomaly

We use the Node.js specific uncaughtException event to try to catch the thrown exception. In this case, uncaught exceptions trigger uncaughtException, which is handled by its callback, and we can see that the program does not exit when uncaughtException is used.

setInterval(() = > {
    console.log('still running');
}, 500)


process.on('uncaughtException'.(err) = > {
    console.log('uncaught exception', err);
});

throw 'simple';

/*
uncaught exception simple
still running
still running
still running
still running
*/
Copy the code

Exception of Promise

Here, things start to get some different: as you can see, in the Promise will not lead to abnormal program exits, the program is still in continue to run, and the Node. Js UnhandledPromiseRejectionWarning warning is given. Of course, this is not consistent with the general programming language, but it is consistent with the common perception of JavaScript.

setInterval(() = > {
    console.log('still running');
}, 500);

async function main() {
    throw 'async';
}

main();

/* (node:27996) UnhandledPromiseRejectionWarning: async (Use `node --trace-warnings ... ` to show where the warning was created) (node:27996) UnhandledPromiseRejectionWarning: ... still running still running still running */
Copy the code

try-catchAbnormal and Promise

Adding try-catch logic to the above code would not catch the exception and cause the program to exit because the main function has no await and will throw the exception after execution

setInterval(() = > { console.log('still running'); }, 500);

async function main() {
    console.log('main start');
    throw 'async';
}

try {
    main();
    console.log('main finish');
} catch (err) {
    console.log(err);
}

/* main start main finish (node:15004) UnhandledPromiseRejectionWarning: async (Use `node --trace-warnings ... ` to show where the warning was created) (node:15004) UnhandledPromiseRejectionWarning: ... still running still running */
Copy the code

Those familiar with the Promise mechanism may know that the logic is processed synchronously before the asynchronous event actually happens in the Promise, that is to say, in our cognition, the execution sequence of the above code should be as follows:

  1. The outputmain start
  2. throwasyncabnormal
  3. The outputmain finish

However, we can actually see that node.js does not handle the unhandledReject exception until the main Start and Main Finish output is complete, that is, the throw in the async function, The corresponding call to Reject in a Promise is actually handled as an asynchronous event externally.

By adding a try-catch to an async throw, we can see that the throw is still executed synchronously, but is delayed as the result of an async throw

setInterval(() => { console.log('still running'); }, 500);
async function main() {
   console.log('main start');
   try {
       throw 'async';
   } catch(err) {
       console.log('catch', err);
   }
}

try {
    main();
   console.log('main finish');
} catch (err) {
   console.log(err);
}
Copy the code

This is done to ensure that promises behave consistently when exceptions are thrown at different places

process.on("uncaughtException")Abnormal and Promise

Node.js does not currently support top-level await, so can an exception thrown by an asynchronous event cause the program to exit? This is of course unreasonable, and we might want to use the previous process.on(“uncaughtException”) to handle it:

setInterval(() = > { console.log('still running'); }, 500);

async function main() {
    throw 'async';
}

main();

process.on('uncaughtException'.(err) = > {
    console.log('uncaught exception', err);
})

/* (node:16181) UnhandledPromiseRejectionWarning: async (Use `node --trace-warnings ... ` to show where the warning was created) (node:16181) UnhandledPromiseRejectionWarning: ... still running still running still running */
Copy the code

Unfortunately, you can see that this is invalid because the exception is an unhandledRejection, not an uncaughtException.

process.on("unhandledRejection")Abnormal and Promise

Of course, it’s not impossible. We can use unhandledRejection to catch this exception event:

setInterval(() = > { console.log('still running'); }, 500);

async function main() {
    throw 'async';
}

main();

process.on('unhandledRejection'.(err) = > {
    console.log('unhandled exception', err);
    throw err;
})

/* unhandled exception async /Users/wwwzbwcom/Downloads/test/test.js:11 throw err; ^ async */
Copy the code

As you can see, the exception can be caught in the event callback of unhandledRejection and thrown again from there, so you need to throw again

Abnormal condition in timer callback

Remember that promises are not the only asynchronous mechanism in JavaScript, but there are also Timer asynchronous events such as setTimeout:

setInterval(() = > { console.log('still running'); }, 500);

setTimeout(() = > {
    throw 'setTimeout';
}, 1000);

/* still running /Users/wwwzbwcom/Downloads/test/test.js:4 throw 'setTimeout'; ^ setTimeout (Use `node --trace-uncaught ... ` to show where the exception was thrown) */
Copy the code

The exception in its callback differs from the one in the Promise, which will be thrown and cause the program to terminate.

The timer callback in the Promise is abnormal

The Promise/timer combination is a bit complicated, but understandable:

  • When the Promise timer callback throws an exception, it throws the top-level exception as normaluncaughtException

Taken together, we can see that

  • The timer callback runs regardless of where the timer is defined
  • The exception mechanism in the timer is the same as the ordinary exception mechanism
setInterval(() = > { console.log('still running'); }, 500);

async function main() {
    setTimeout(() = > {
        throw 'setTimeout in async';
    }, 1000);
}

main();

/* still running /Users/wwwzbwcom/Downloads/test/test.js:5 throw 'setTimeout in main'; ^ setTimeout in main (Use `node --trace-uncaught ... ` to show where the exception was thrown) */
Copy the code

Promise exception in timer callback

As mentioned above, the exception handling in the timer is the same as ordinary exception cases, so the async exception in the timer callback is also an unhandledRejection event:

setInterval(() = > { console.log('still running'); }, 500);

setTimeout(function () {
    main();
}, 1000);

async function main() {
    throw 'async in setTimeout'
}

/* still running (node:18741) UnhandledPromiseRejectionWarning: async in setTimeout (Use `node --trace-warnings ... ` to show where the warning was created) (node:18741) UnhandledPromiseRejectionWarning: ... still running still running */
Copy the code

Exceptions in the Worker

The Worker mechanism of Node.js increases the complexity of exception handling, but except for the characteristics of Worker itself, it still has the same logic as other exception handling. Let’s sort out this part clearly:

Common exceptions in Worker

If we throw a normal exception in the Worker thread, we can see:

  • The Worker thread stops running
  • The main thread received a message from the Worker threaderrorThe event
  • The main thread does not stop running
const { Worker, isMainThread } = require('worker_threads');

if (isMainThread) {
		setInterval(() = > { console.log('main still running'); }, 500);
    const worker = new Worker(__filename)
    worker.on('error'.(err) = > {
        console.log('worker error event: ' + err);
    });
} else {
    setInterval(() = > { console.log('worker still running'); }, 500);
    throw 'worker error';
}

/*
worker error event: Error: Unhandled error. ('worker error')
main still running
main still running
main still running
main still running
*/
Copy the code

The Promise exception in the Worker

When a Promise exception occurs in a Worker, it is similar to when an asynchronous exception occurs in the main thread. The Worker thread will not terminate, let alone the program

const { Worker, isMainThread } = require('worker_threads');


if (isMainThread) {
    setInterval(() = > { console.log('main still running'); }, 500);
    const worker = new Worker(__filename)
    worker.on('error'.(err) = > {
        console.log('worker error event: ' + err);
    });
} else {
    setInterval(() = > { console.log('worker still running'); }, 500);
    async function main() {
        throw 'worker async error';
    }
    main();
}

/* (node:19866) UnhandledPromiseRejectionWarning: worker async error (Use `node --trace-warnings ... ` to show where the warning was created) (node:19866) UnhandledPromiseRejectionWarning: ... main still running worker still running main still running worker still running main still running worker still running * /
Copy the code

Description The timer callback in the Worker is abnormal

This is similar to when an exception occurs in the main thread timer callback, the Worker thread does not terminate, let alone cause the program to terminate

const { Worker, isMainThread } = require('worker_threads');


if (isMainThread) {
    setInterval(() = > { console.log('main still running'); }, 500);
    const worker = new Worker(__filename)
    worker.on('error'.(err) = > {
        console.log('worker error event: ' + err);
    });
} else {
    setInterval(() = > { console.log('worker still running'); }, 500);
    setTimeout(() = > {
        throw 'worker setTimeout error'
    }, 1000);
}

/*
main still running
worker still running
main still running
worker error event: worker setTimeout error
main still running
main still running
main still running
main still running
*/
Copy the code

conclusion

The asynchrony mechanism in JavaScript varies from throw exceptions, which are similar to other rendering languages, to exceptions generated in asynchronous events.

The emergence of Node.js introduces more abnormal situations, such as those generated in workers. More exception handlers have also been introduced, including process.on(“uncaughtException”) and process.on(“unhandledRejection”).

Although these exceptions can be analyzed and derived with a good understanding of these asynchronous and exception mechanisms, they often lead us into confusion in practical development. Hopefully, this article can list all kinds of strange exceptions while using the analysis of these cases. Sort out the internal logic of node.js asynchronous mechanism and exception mechanism.