Skip to content

Instantly share code, notes, and snippets.

@ilyaigpetrov
Last active January 26, 2020 14:55
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ilyaigpetrov/96a4491877f4bd14f95cf41134de1990 to your computer and use it in GitHub Desktop.
Save ilyaigpetrov/96a4491877f4bd14f95cf41134de1990 to your computer and use it in GitHub Desktop.
Ways of Handling Rejected Promises in JavaScript | https://git.io/js-rejections | by https://git.io/ilyaigpetrov

Ways of Handling Rejected Promises in JavaScript

This note/example shows 3 different ways of handling promise rejections in JavaScript.

Before we start you may want to refresh in your mind that:

  1. .then(...) takes two arguments: MDN.
  2. .catch(...) is a syntactic surgar around .then(...) with two arguments: MDN.
  3. Exploring JS:

    Exceptions that are thrown in the callbacks of then() and catch() are passed on to the next error handler, as rejections

In the contrived example below we want:

  1. Get JSON response and check if it conforms to our little schema.
  2. Report errors which must be fixed by a programmer.
  3. If server responses with a 4xx (client errors) then report it too.
'use strict';

const fetch = require('node-fetch');

// In the real world here may be some function
// that is very complex and error prone!
const isJsonValidDangerous = (json) => json.ok === true;

const is4xx = (res) => 400 <= res.status && res.status < 500;
const reject4xxAsync = async (res) => {

  if (is4xx(res)) {
    return res.text()
      .then(
        (text) => {

          // Throwing error inside promise callback results in a promise rejection
          // with the thrown error as a rejected value.
          throw new Error(`${res.status}: ${text}`);
        },
      );
  }
  return res;
};
const reportErrorAsync = (err) => {

  console.log('Reporting error:', err);
  // This function always resolves, even if reporting fails.
  // Example of implementation:
  // return fetch(...)
  //   .catch((reportingErr) => {/* Show error to the user in a robust way. */})
  //   .catch(() => {/* Suppress. */});
  return Promise.resolve();
};

/*
==========================================
Way 1: .then(...) with the second argument
==========================================
Con: Doesn't handle all errors, see the comment below.
*/
const doesHaveValidJsonAsync1 = async (url) =>
  fetch(url)
    .then(reject4xxAsync)
    .then((res) => res.json())
    .then(
      isJsonValidDangerous, // May throw an error, which won't be handled!
      (err) => reportErrorAsync(err)
        .then(() => false), // After reporting we want to resolve with `false`.
    );

/*
==================
Way 2: .catch(...)
==================
Pro: Handles all errors.
*/
const doesHaveValidJsonAsync2 = async (url) =>
  fetch(url)
    .then(reject4xxAsync)
    .then((res) => res.json())
    .then(isJsonValidDangerous) // May throw an error, will be handled by the next line.
    .catch(
      (err) => reportErrorAsync(err)
        .then(() => false),
    );

/*
==============================================
Way 3.1: try...catch + many awaits - then(...)
==============================================
Con: Imperative code (vs. expressions) may be less composable (practical example needed).
Con: `await` may be not supported on your target browser in contrast to promises, which are older.
Pro: Some people find it easier to read compared to a chain of then/catch.
*/
const doesHaveValidJsonAsync3_1 = async (url) => {

  try {
    const res     = await fetch(url);
    const goodRes = await reject4xxAsync(res);
    const json    = await goodRes.json();
    return isJsonValidDangerous(json);
    // Or if you like nesting: return isJsonValidDangerous(await goodRes.json());
  } catch(err) {
    await reportErrorAsync(err);
    return false;
  }
};
/*
=================================================
Way 3.2: try...catch + mixing await and then(...)
=================================================
Pros & cons: See way 3.1.
Con: Some people advocate against mixing await and then/catch.
*/
const doesHaveValidJsonAsync3_2 = async (url) => {

  try {
    // At least one `await` is needed for a rejection to be thrown and caught by try...catch.
    const ifValid = await fetch(url)
      .then(reject4xxAsync)
      .then((res) => res.json())
      .then(isJsonValidDangerous);
    return ifValid;
    // A shorter variant: return await fetch(...)...
  } catch(err) {
    await reportErrorAsync(err);
    return false;
  }
};

(async () => {

  //=======
  // Tests
  //=======

  // For manual testing:
  /*
  { // <- namespace opens.
    const url = 'https://gist.githubusercontent.com/ilyaigpetrov/96a4491877f4bd14f95cf41134de1990/raw/f00434edd42b7029fec587507ec585e42930c730/zzz-test-ok.json';
    const ifValid = await doesHaveValidJsonAsync3_2(url);
    console.log(`${url} has`, ifValid ? 'valid json' : 'NO valid json');
  }
  */

  // Non-manual tests.
  const funcs = [
    doesHaveValidJsonAsync1,
    doesHaveValidJsonAsync2,
    doesHaveValidJsonAsync3_1,
    doesHaveValidJsonAsync3_2,
  ];
  const runTestAsync = async (testName, url, expected, message) => {

    console.log(`Test "${testName}" started.`);
    const promises = funcs.map(async (func) => {

      const actual = await func(url);
      console.assert(
        actual === expected,
        `${func.name}: ${message}`,
      );
    });
    await Promise.all(promises);
    console.log(`Test "${testName}" finished.\n`);
  };
  console.log(/* Empty line. */);

  // Test 1: Valid JSON
  const urlOk = 'https://gist.github.com/ilyaigpetrov/96a4491877f4bd14f95cf41134de1990/raw/3bbe333cecd0f17b1189e8ede558b69587d44269/zzz-test-1-ok.json';
  await runTestAsync(
    'Valid JSON',
    urlOk,
    true,
    '`true` must be returned for a valid JSON structure.',
  );

  // Test 2: Non Valid JSON
  const urlNotOk = 'https://gist.github.com/ilyaigpetrov/96a4491877f4bd14f95cf41134de1990/raw/3bbe333cecd0f17b1189e8ede558b69587d44269/zzz-test-2-not-ok.json';
  await runTestAsync(
    'Non Valid JSON',
    urlNotOk,
    false,
    '`false` must be returned for a non-valid JSON structure.',
  );

  // Test 3: 4xx
  await runTestAsync(
    '4xx',
    'https://httpstat.us/403',
    false,
    '`false` must be returned for 4xx.',
  );
})();

Ways of Making This Code Better

To make this example easier some real world issues are not handled:

  1. Handle cases when server responses with something that is not JSON, report such cases together with the server response. You can't use res.json() and in the case of failure res.text() on the same response, instead you may:

    1. Use res.text(), then try { JSON.parse(text); } catch(err) { /* Report the text, which wasn't parsed. */ }.
    2. Use res.clone().

    At some point you probably will want to write a fetch wrapper for this and other things.

  2. I wouldn't embed error reporter into every function, instead I would let functions reject and handle rejections with a reporter on the most external level.

  3. Report 5xx (server errors) as well.

  4. Network failures result in fetch rejections which are reported to the programmer. If this code is executed on a client (not on a server) then network failures should be reported to the user only.

Publications

Thank You, Contributors

I've asked many people on freenode, gitter and reddit to find out which way they advocate.
I want to thank everyone who participated.
Special thanks for a review to Sergey Zhigalov (@Zhigalov).

{ "ok": "This non-empty string is strictly not `true`." }
@ilyaigpetrov
Copy link
Author

ilyaigpetrov commented Sep 21, 2018

I import a reply from @Zhigalov here

Ты молодец что затронул тему обработки исключений. Это очень важно понимать, когда пишешь асинхронный код в продакшне. Давай постараемся рассказать от этом ещё понятнее.

Что мне понравилось

  1. В примере есть два типа ошибок: синхронная, которая получается при парсинге JSON, и асинхронная, которая прилетает с сервера. Это самый правильный подход объяснить проблему. Тут ты очень тонко и верно подобрал пример, круто

  2. Все обработчики ты вынес в функции с говорящими названиями, написанными в одну строчку. Это понятно и читаемо, так мне было легко разобраться с кодом. Это однозначно надо оставить как есть.

  3. Мне очень понравился переход от первого примера ко второму. Он верно выстроен. В этот переход я бы добавил теории. Не все знают, что в then можно передать два обработчика. Надо рассказать что это значит и в каком порядке они срабатывают. У Ильи Кантора есть замечательный пример c Identity и Thrower, я бы позаимствовал оттуда и теорию и идею для иллюстрации.

Что мне НЕ понравилось

  1. Пример 3.1, он режет глаз. Я стараюсь не использовать в одной конструкции промисы и async/await и всячески отговариваю своих коллег это делать. Понимаю, что ты хочешь сделать переход от первому ко второму, но как по мне - не стоит. Я бы убрал.

  2. Библиотека node-fetch имеет несколько замудрённый интерфейс. Ты сначала асинхронно ждёшь ответа, а потом вызываешь res.text() который снова асинхронный. В продакшне использовать - ок, без вопросов. Но в примере надо объяснить максимально просто. Не надо добавлять лишних асинхронностей, чтобы не запутать читателя. Может заменить на got. У неё и звёзд больше, и второй асинхронности нет.

  3. Не понял зачем

     (err) => reportErrorAsync(err)
        .then(() => false),

Может зачейнить на уровень выше или вообще убрать.

Предложения

  1. Может в reject4xxAsync не оборачивать ошибку в Promise.reject, а сразу её кинуть при помощи throw. Так ты ещё раз на примере покажешь что синхронные ошибки внутри обработчиков промиса становятся асинхронными.

My reply to his post

Большое спасибо за подробный отклик на заметку.

  1. В примере есть два типа ошибок: синхронная, которая получается при парсинге JSON...

Парсинг JSON через response.json() работает асинхронно (возвращает Promise), а вот isJsonValidDangerous(...) работает синхронно и может прокинуть ошибку. Верно, что это пример синхронной ошибки в обработчике Promise. У Ильи Кантора используется .then(JSON.parse), но я не уверен, что ради примера нужно отступать от принятого в языке решения/стиля/идиомы, по крайней мере без предупреждения читателя об этом.

Не все знают, что в then можно передать два обработчика. Надо рассказать что это значит и в каком порядке они срабатывают.
У Ильи Кантора есть замечательный пример c Identity и Thrower.

Пример хороший, но я сторонник того, что программисту нужно развивать технический английский и читать/писать на нём с самого начала.
Может, MDN и менее подробный источник, но лучшего у меня пока нет. Добавил пару ссылок на MDN во вступительный абзац.

  1. Пример 3.1, он режет глаз. Я стараюсь не использовать в одной конструкции промисы и async/await и всячески отговариваю своих коллег это делать.

await применяется как раз таки к Promise, думаю, ты имеешь ввиду не использовать then/catch и await в одной конструкции. Я поменял 3.1 и 3.2 местами, теперь 3.2 — это пример не всеми рекомендуемого стиля с соответствующей пометкой.

  1. Библиотека node-fetch имеет несколько замудрённый интерфейс.
    ...
    Может заменить на got. У неё и звёзд больше, и второй асинхронности нет.

Я рассчитываю, что заметка может быть полезной не только программистам на Node.js, но и фронтенд-программистам на JavaScript.
Потому я бы использовал API, который поддерживается и браузером и Node.js (cross-fetch, node-fetch).

  1. Не понял зачем
const doesHaveValidJsonAsyncX = ...
...
     (err) => reportErrorAsync(err)
        .then(() => false),

doesHaveValidJsonAsyncX(...) возвращает Boolean:

  • строго true — если по адресу есть валидный JSON,
  • строго false — во всех других случаях (даже если возникла неожиданная ошибка и мы о ней доложили или не доложили).

В продакшн я бы вынес докладчик на самый внешний уровень и не встраивал бы его таким образом в каждую функцию.
Добавил это как рекомендацию под пунктом 2 (Ways of Making This Code Better):

  1. I wouldn't embed error reporter into every function, instead I would let functions reject and handle rejections with a reporter on the most external level.
  1. Может в reject4xxAsync не оборачивать ошибку в Promise.reject, а сразу её кинуть при помощи throw.

Исправил на throw, для объяснения добавил ссылку на ExploringJS и комментарий.

Также

Добавил тесты.
Добавил тебя в раздел благодарностей.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment