Now Reading
The gotcha of unhandled promise rejections

The gotcha of unhandled promise rejections

2023-01-11 18:12:08

As an example you wished to show a bunch of chapters on the web page, and for no matter cause, the API solely provides you a chapter at a time. You can do that:

async operate showChapters(chapterURLs) {
  for (const url of chapterURLs) {
    const response = await fetch(url);
    const chapterData = await response.json();
    appendChapter(chapterData);
  }
}

This provides the right end result – all of the chapters seem in the appropriate order. However, it is kinda sluggish, as a result of it waits for every chapter to complete fetching earlier than it tries to fetch the following one.

Alternatively, you possibly can do the fetches in parallel:

async operate showChapters(chapterURLs) {
  const chapterPromises = chapterURLs.map(async (url) => {
    const response = await fetch(url);
    return response.json();
  });

  const chapters = await Promise.all(chapterPromises);

  for (const chapterData of chapters) appendChapter(chapterData);
}

Nice! Besides, you are now ready on the final chapter earlier than displaying the primary.

For the most effective efficiency, do the fetches in parallel, however deal with them in sequence:

async operate showChapters(chapterURLs) {
  const chapterPromises = chapterURLs.map(async (url) => {
    const response = await fetch(url);
    return response.json();
  });

  for await (const chapterData of chapterPromises) {
    appendChapter(chapterData);
  }
}

Nonetheless, this has launched a difficult bug involving unhandled promise rejections.

Unhandled promise rejections occur when a promise… is rejected… however is not dealt with.

Okay okay, they’re just like the promise equal of an uncaught error. Like this:

const promise = Promise.reject(Error('BAD'));

The rejected state of this promise is ‘unhandled’ as a result of nothing is coping with the rejection.

Listed here are just a few methods it might be dealt with:


attempt {
  await promise;
} catch (err) {
  
}


promise.catch(() => {
  
});


await promise;




promise.then(() => {
  
});


A promise is dealt with when one thing is completed in response to that promise, even when it is creating one other rejected promise, or turning a rejected promise right into a throw.

As soon as a promise is rejected, you have got till just-after the following processing of microtasks to deal with that rejection, else it could rely as an unhandled rejection (‘could’, as a result of there’s a bit little bit of wiggle room with process queuing).

const promise = Promise.reject(Error('BAD'));




queueMicrotask(() => {
  
});

setTimeout(() => {
  
}, 0);

Unhandled rejections are problematic

Unhandled rejections are a bit like uncaught errors, in that they trigger your entire program to exit with an error code in Node and Deno.

In browsers, you get errors showing within the console, once more just like uncaught errors:

In the console: Uncaught (in promise) TypeError: Failed to fetch

They could additionally seem in error logging methods, if the system listens for unhandled rejections:

addEventListener('unhandledrejection', (occasion) => {
  
});

The purpose is, you wish to keep away from unhandled rejections.

It isn’t instantly apparent:

async operate showChapters(chapterURLs) {
  const chapterPromises = chapterURLs.map(async (url) => {
    const response = await fetch(url);
    return response.json();
  });

  for await (const chapterData of chapterPromises) {
    appendChapter(chapterData);
  }
}

The guarantees in chapterPromises are dealt with by the for await in every iteration of the loop. When the loop encounters a rejected promise, it turns into a throw, which abandons the operate and rejects the promise showChapters returned.

The bug occurs if a promise rejects earlier than the for await handles that promise, or if that promise isn’t reached.

For instance: If chapterPromises[0] takes a very long time to resolve, and in the meantime chapterPromises[1] rejects, then chapterPromises[1] is an unhandled rejection, as a result of the loop hasn’t reached it but.

Or: If chapterPromises[0] and chapterPromises[1] reject, then chapterPromises[1] is an unhandled rejection, as a result of the loop is deserted earlier than it will get to chapterPromises[1].

Ugh. The “unhandled promise rejection” function is there so you do not ‘miss’ rejected guarantees, however on this case it is a false optimistic, as a result of the promise returned by showChapters already sufficiently captures the success/failure of the operation.

This challenge would not at all times contain fetches, it might be any bit of labor you begin early, then choose up the end result later. Like a employee process.

This does not at all times contain for await both. It impacts any state of affairs the place you begin the work early, then deal with the end result later, asynchronously.

It wasn’t a difficulty for the Promise.all instance:

See Also

async operate showChapters(chapterURLs) {
  const chapterPromises = chapterURLs.map(async (url) => {
    const response = await fetch(url);
    return response.json();
  });

  const chapters = await Promise.all(chapterPromises);

  for (const chapterData of chapters) appendChapter(chapterData);
}

On this case all the guarantees in chapterPromises are dealt with instantly, by Promise.all, which returns a single promise that is instantly dealt with by the await. However this answer has worse efficiency than our sequential answer.

Sadly it is a bit of a hack. The answer is to instantly mark the guarantees as dealt with, earlier than they’ve an opportunity to develop into unhandled rejections.

async operate showChapters(chapterURLs) {
  const chapterPromises = chapterURLs.map(async (url) => {
    const response = await fetch(url);
    return response.json();
  });

  

  for await (const chapterData of chapterPromises) {
    appendChapter(chapterData);
  }
}

A technique to do that is so as to add a dummy catch handler to every promise:

for (const promise of chapterPromises) promise.catch(() => {});

This does not change the guarantees aside from marking them as ‘dealt with’. They’re nonetheless rejected guarantees. It would not trigger errors to be missed/swallowed elsewhere.

A shorter solution to obtain the identical factor is Promise.allSettled:

Promise.allSettled(chapterPromises);

This works as a result of allSettled handles all the guarantees you give it, just like Promise.all, however in contrast to Promise.all it by no means returns a rejected promise itself (until one thing is essentially incorrect with the enter iterator).

Each of those look fairly hacky, and more likely to confuse others that learn the code later. Due to this, I might most likely create a helper operate like preventUnhandledRejections:


export operate preventUnhandledRejections(...guarantees) {
  for (const promise of guarantees) promise.catch(() => {});
}

And remark its utilization:

import { preventUnhandledRejections } from './promise-utils.js';

async operate showChapters(chapterURLs) {
  const chapterPromises = chapterURLs.map(async (url) => {
    const response = await fetch(url);
    return response.json();
  });

  
  
  preventUnhandledRejections(...chapterPromises);

  for await (const chapterData of chapterPromises) {
    appendChapter(chapterData);
  }
}

I want there was a much less ‘blunt’ means of dealing with this in JavaScript, however I am unsure what that may appear like. The design of the “unhandled rejections” function instantly clashes with beginning work early and dealing with the end result later, or not dealing with the end result if a prerequisite fails.

Within the meantime, preventUnhandledRejections does the trick!

For completeness, this is an abortable implementation of showChapters, that additionally handles dangerous responses.

Because of Surma and Thomas Steiner for proof-reading.



Source Link

What's Your Reaction?
Excited
0
Happy
0
In Love
0
Not Sure
0
Silly
0
View Comments (0)

Leave a Reply

Your email address will not be published.

2022 Blinking Robots.
WordPress by Doejo

Scroll To Top