4

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?

fekaloid
  • 195
  • 1
  • 2
  • 9
  • 1
    "*since there is no registered rejection handler the control sequentially moves on to the next line.*" - can you explain what you mean by this? Control always moves on to the next line, if there were rejection handlers on the promise they only might get scheduled. – Bergi Sep 12 '20 at 13:33
  • Oh, really, you are right. Thanks for noticing. – fekaloid Sep 12 '20 at 14:04

2 Answers2

3

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.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
2

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.

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 1
    Also worth mentioning our design for unhandled rejections is heuristic and it's "it's unhandled if no catch handler was attached synchronously or within a microtick". Chrome uses a similar heuristic, firefox maybe still uses GC (I think?). – Benjamin Gruenbaum Sep 12 '20 at 10:24
  • Thank you. That's a good explanation. So, when it reaches the end of the script and before the timeout is complete the microtasks queue is about to kick in, which would be empty in our case. Since the rejection is unhandled then I indeed can see the error being on the screen if I open up the console. But I can't quite get it how the rejection gets to the handler one second after if it has not been kept in memory. Sorry if that's a foolish question I'm asking, maybe there is something fundamental I'm missing you can point me out to so it would clarify the situation. – fekaloid Sep 12 '20 at 11:38
  • 1
    I don't think the OP is asking about unhandled rejection handling. – Bergi Sep 12 '20 at 13:34