How does javascript manage to catch exception in late .catch handler?

Viewed 136

The other day I came across the following peace of code:

let promise = Promise.reject(new Error('Promise Failed!')); // 1

setTimeout(() => promise.catch(err => alert('caught')), 1000); // 2

// 3

So I was quite surprised to find out the error was caught in the handler registered one second after the error had occurred.

Now, let me explain step by step the way I perceive this, so you could correct me and tell me where I'm wrong:

  1. During the current macrotask, which is executing this whole script, a promise rejects (1) and since there is no registered rejection handler the control sequentially moves on to the next line.
  2. We call setTimeout passing it a callback function where rejection handler is to be registered (2). But this callback will be scheduled only after a second delay. Furthermore, it will be executed within a separate task.
  3. The execution is on line 3. The current macrotask is done, both stack and task queue are empty. All that's left is to wait for a given delay of 1 second.
  4. One second timeout is complete. Our callback gets to the task queue and right away it is being pushed to the stack which effectively runs it.
  5. During the execution of the callback function, which is part of a new task, .catch handler is registered for the promise rejected one second ago, which took place during the previous and already finished task. Nevertheless, it successfully catches the error.

Does this mean that all this time the error had been somewhere in memory waiting for a chance that maybe .catch handler would be registered later? But what if it had never happened and the number of rejected promises had been a lot more? Will unhandled errors remain to 'hang' in memory waiting for its handler to be registered?

2 Answers

Does this mean that all this time the error had been somewhere in memory waiting for a chance that maybe .catch handler would be registered later?

Yes, it is stored in the promise object. Notice that promises are not just a notification mechanism, they are meant to represent the result of a one-off asynchronous task. The fulfillment value or rejection reason (in your case, the Error instance) and the resolution state are stored on the promise when it settles.

The main idea behind this is that a .then or .catch handler will always be called with the result value, regardless whether the .then() call happens before or after the settling of the promise. This also allows multiple handlers.

Will unhandled errors remain to 'hang' in memory waiting for its handler to be registered?

If you didn't have the setTimeout, the let promise variable and with it the promise object and the error object would have been garbage-collected immediately.

to answer your question directly:

Does this mean that all this time the error had been somewhere in memory waiting for a chance that maybe .catch handler would be registered later?

Yes, we have a WeakMap of pending rejections that keeps track of promises that were rejected but not synchronously handled. Chrome does something similar (I can link to it if you want).

But what if it had never happened and the number of rejected promises had been a lot more?

The assumption in the design of these features is that rejections are pretty rare and are for exceptional cases - so the rejection path is kind of slow. But yes, you could theoretically create a lot of "pending" rejections.

Will unhandled errors remain to 'hang' in memory waiting for its handler to be registered?

We only wait for a microtick - so the flow is:

  • You create a rejected promise
  • It has no handler, so it gets put in the WeakMap
  • All microtasks are run (process.nextTick/Promise.resolve.then)
  • If it is still rejected, it is an unhandled rejection and it gets logged to the screen / the event is fired.

That is, the rejection is not kept "in memory" for 1000ms (the timer duration) but just for as long as it takes for microtasks to run.

Related