Why asynchronous exceptions are uncatchable with callbacks
You have a block of code that throws an exception. You have tried putting it inside a try..catch
block, but it still does not get caught. You have been told the error is thrown asynchronously, but you are not sure what is exactly going on. You would like to understand why your code doesn't catch it.
What is asynchronous code
An asynchronous function is a function that performs an operation in the background. It leverages services provided by the JavaScript host and the operating system. What is visible in JavaScript to such call is 1) the initial setting up and initialization of the call and 2) the point when the callback is called and the operation is complete.
Throwing an error asynchronously means that an error is thrown in the JavaScript code of an asynchronously executed callback.
Throwing exception asynchronously
The following code reproduces the example.
Here a try.. catch
block is used to wrap a call to setImmediate()
. It is a function that operates asynchronously and schedules the argument callback to be called in the near future, as soon as other operations have finished. There are no other statements to execute, and the control is passed back to one level up — to the Node.js event loop.
Pseudocode of event loop
In order to better understand what is taking place one level up from the user code, let's write the event loop in JavaScript pseudocode. Pseudocode is something that describes a concept at a higher level, possibly omitting implementation details. In real life, the event loop in Node.js is written in C.
The event loop is an infinite while
loop that is entered as the last thing when Node.js process is started. Its responsibility is to repeatedly take events and fire any listeners registered for them one at a time. Listeners are called sequentially - one at a time.
It can be described by combining two API functions in a while loop.
Here getNextEvent()
returns the next event. If there are no events available, it will block until new events occur. The getListeners()
function returns an array of listeners that were waiting for the given event.
Run in two passes
Let's go back to the example. We can see there's the initial user code that makes the setImmediate()
-call. The anonymous function passed as argument is not executed at this point, and it is only merely passed as an argument like a number, or a string would be passed.
We can think the highlighted code is executed in one pass. After the pass has finished, the control is passed one level up. Then, after other operations have finished, the setImmediate()
contract is fulfilled, and the following section is executed.
If we combine the highlights, we can see the entire file actually contains pieces that are executed at different times.
Visualize executed passes by including surrounding event loop code
Let's combine this with the event loop pseudocode. We will replace the listener calling in the pseudocode with the actual code that will be executed. The first pass looks like
And the second part, after the "immediately after other operations have finished" event has occurred, looks like
From the second part, it can be seen that the original catch block that was intended to be there is not present. When the code is executed asynchronously, the original synchronous catch block is not present. In this case, the exception will propagate all the way up to the Node.js internals and will cause the program to exit prematurely.
Catch block is not present when asynchronous callback is executed
An asynchronous exception is uncatchable because the intended catch block is not present when the asynchronous callback is executed. Instead, the exception will propagate all the way and terminate the program.
Related articles
- Does taking a callback make a function asynchronous?
- Event loop from 10,000ft - core concept behind Node.js
Semantic Versioning Cheatsheet
Learn the difference between caret (^) and tilde (~) in package.json.