1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

同期処理、Promise、Async/Awaitで相互にエラーハンドリングする一覧

Posted at

同期処理と非同期処理の間でエラーを渡す

エラー処理の引き渡しの資料はQiitaでもTipsとしていっぱい転がっている。しかし可能性として有りうる同期処理・Promise・Async/Await相互のエラー受け渡しについて言及したものは少ない。そこで、それらをできるだけ網羅してみたつもりである。
読みやすくなる前提知識は以下。

  • 自前のエラーはインスタンスの生成(new Error)で作る
  • エラーはthrowで投げる
  • 同期処理中のエラーはtry-catch構文で拾う
  • PromiseのエラーはPromise.catchメソッドで拾う
  • Async関数内のエラーはthrowで投げ、try-catch構文で拾う

この上で、コードと実行結果を交えて、Uncaughtなエラーをうまく扱っていく。

調べたきっかけ

とあるOSSを使ってみたが、非同期処理の結果が返ってこない。そのときはエラー処理が問題なのかどうかの切り分けができず、苦労することに。

イベントループ、try-catch、非同期処理、Async関数、エラーオブジェクト…

エラーをハンドルしたいだけなのに、なぜこうもMDNのドキュメントを行き来せねばならず、加えて Promise.all を紹介されなければならないのか。
それらを深追いした末に、エラーを非同期処理中で発生させたものを同期処理中にしっかりキャッチしてもらうコードを練習するに至った。

投稿のきっかけ

私はN予備校生のsatsukizzzです。N予備校のWebプログラミング講座では、Node.jsを用いてサーバーサイドを含めJavaScriptで書くという経験をしました。その後に上記のようなことが起きました。
せっかくある程度のレベルで調べ上げたわけなので、N校受講生用の2020年アドベントカレンダーに…と思っていたのに。年末のやっつけ仕事でやろうとした私がアホで、12/25を最後にアドベントカレンダーは閉幕していました。
一方で私の友人関係では同じようにJavaScriptを使っている人たちも多いため、遅ればせながら投稿することとなりました。

環境とブラウザ

  • クライアントサイドのJavaScript
  • Google Chrome 86.0.4240あたり / Firefox 81.0.1あたり

Node.jsでは確認していない(next関数も重要であることは承知している)。
なお、全てのエラー表示の画像はGoogle Chrome 87.0.4280のDeveloper Toolを用いて取得している。

ケース

同期処理から同期処理へ

const tryThrownErrorInSyncToSync = () => {
  try {
    throw new Error('in sync');
  } catch (e) {
    console.error(e);
  }
}
tryThrownErrorInSyncToSync();

image.png

Promiseから同期処理へ(uncaught)

const tryRejectedErrorInPromiseToSync = () => {
  try {
    new Promise((resolve, reject) => reject(new Error('in promise; this is an uncaught error')));
  } catch (e) {
    console.error(e);
  }
}
tryRejectedErrorInPromiseToSync();

image.png

Async関数から同期処理へ

const rejectAsync = async (errorMessage) => {
  throw new Error(errorMessage);
}; // rejectAsync関数は実はPromiseのエラーを返すなら何にでも変えられる
const throwPromise = (errorMessage) => {
  return new Promise((resolve, reject) => reject(new Error(errorMessage)));
}; // つまり、代わりにこのthrowPromise関数でもよい

const tryThrownErrorInAsyncToSync = async () => {
  try {
    rejectAsync('uncaught async error in reject async');
  } catch (e) {
    console.error(e);
  }

  try {
    await rejectAsync('caught async error in reject async');
  } catch (e) {
    console.error(e);
  }
  return;
}

tryThrownErrorInAsyncToSync();

image.png
image.png

同期処理からPromiseへ;rejectで

new Promise((resolve, reject) => {
  reject(new Error('catch rejected error in sync to promise'))
})
.catch(console.error);

image.png

同期処理からPromiseへ;throwで

new Promise((resolve, reject) => {
  throw new Error('catch thrown error in sync to promise; same as rejected one');
})
.catch(console.error);

image.png

PromiseからPromiseへ;コメント付加

//if you need a Promise object with arguments then make a function which returns Promise object. put that function object in Promise.then or Promise.catch directly as below
const returnRejectedPromise = (error) => {
  return new Promise((resolve, reject) => {
    error.message = 'rejected in promise, and ' + error.message;
    reject(error);
  });
}
Promise.reject(new Error('catch rejected error in promise chain'))
.catch(returnRejectedPromise) // added some information and re-rejected
.catch(console.error); // executed

new Promise((resolve, reject) => {
  returnRejectedPromise(new Error('cannot catch rejected error in promise to promise; please chain those promises'));
})
.catch(console.error); // not executed

image.png
image.png

Async関数からPromiseへ;コメント付加

// using async function instead of returnRejectedPromise
const throwPromiseWithAsync = async (error) => {
  error.message = 'thrown in async, and ' + error.message;
  throw error;
}
Promise.reject(new Error('catch rejected error in promise chain'))
.catch(throwPromiseWithAsync) // added some information and re-rejected(thrown in async)
.catch(console.error); // executed

new Promise((resolve, reject) => {
  throwPromiseWithAsync(new Error('cannot catch thrown error in async to promise; please chain those promises'));
})
.catch(console.error); // not executed

image.png
image.png

Promiseから同期処理へ;Resolveしてしまった場合

const promiseCatchResolvedErrorInPromiseToSync = () => {
  new Promise((resolve, reject) => {
    resolve(new Error('resolved with an error in promise; this is caught in fulfilled'));
  })
  .then(value => {console.log(value);}) // catches as "value" of the error
  .catch(error => {console.error(error);}); // not executed
}
promiseCatchResolvedErrorInPromiseToSync();

image.png
つまり、もしエラー終了させたいならthen(value => {console.error(value);})としてやったらいいとおもう。病気。

Promiseから同期処理へ

const promiseCatchRejectedErrorInPromiseToSync = () => {
  new Promise((resolve, reject) => {
    reject(new Error('rejected with an error in promise'));
  })
  .catch(e => console.error(e));
}
promiseCatchRejectedErrorInPromiseToSync();

image.png

Promiseから同期処理へ;エラーオブジェクトを生成しなかった場合

const promiseCatchRejectedInPromiseToSync = () => {
  new Promise((resolve, reject) => {
    reject('rejected without an error in promise');
  })
  .catch(e => console.error(e));
}
promiseCatchRejectedInPromiseToSync();

▼を押して開くと現れる、ブラウザによるスタックトレースのみとなる(エラーオブジェクトのスタックトレースがない)。
image.png

async関数からPromiseへ;returnした場合

//Promise内でAsync functionを実行しエラーを伝える
const asyncReturnError = async () => {
  return new Error('returned with an error in async; this is caught in fulfilled');
};

new Promise((resolve, reject) => {
    asyncReturnError()
    .then(value => {resolve(value);}) //executed
    .catch(error => {reject(error);});
})
.then(value => {console.log(value);}) //executed
.catch(error => {console.error(error);});

image.png

エラーオブジェクトのスタックトレースが表示されるが、エラーの色をしていない。
また、ブラウザのスタックトレースが生成されない。

async関数からPromiseへ;throwした場合

// throwの場合
const asyncThrowError = async () => {
  throw new Error('throw an error in async; this is caught in rejected');
};

new Promise((resolve, reject) => {
    asyncThrowError()
    .then(value => {resolve(value);})
    .catch(error => {reject(error);}); //executed
})
.then(value => {console.log(value);})
.catch(error => {console.error(error);}); //executed

image.png

promiseからasyncへ

//make errors in Promise and transfer them to Async function
async function testErrorsInPromiseToAsync() {
  try {
    await new Promise((resolve, reject) => {
      reject(new Error('rejected in promise to async try block with await(but not useful because the scope of variables is try)'));
    });
  } catch (error) {
    console.error(error);
  }
  
  new Promise((resolve, reject) => {
    reject(new Error('error in rejected promise to async function without await; this is uncaught'));
  })
  .catch(error => {throw error;});

  await new Promise((resolve, reject) => {
    reject(new Error('error in rejected promise to async function with await'));
  })
  .catch(error => {throw error;});

}
testErrorsInPromiseToAsync()
.catch(console.error);

上から順に
image.png
image.png
image.png

おまけ: setTimeout のラップ

promiseでsetTimeoutをラップ

「setTimeoutはPromiseでラップする」とは例えばこのように。

const delayedResolve = (ms) => {
  return new Promise(resolve => {setTimeout(resolve, ms)});
};

const delayedReject = (ms, error) => {
  return new Promise((resolve, reject) => {setTimeout(reject(error), ms)});
};

そのうえで、

const throwAfterTimeout = async () => {
  await delayedReject(1000, new Error('error in trying delayedReject'))
  .catch(console.error);
}

const throwInSetTimeout = async () => {
  try {
    await setTimeout(() => {
      throw new Error('error in trying setTimeout; this is uncaught');
    }, 2000);
  } catch (error) {
    console.error(error);
  }
}

const promiseCatchAfterTimeout = () => {
  delayedReject(3000, new Error('error in Promise.catching delayedReject'))
  .catch(console.error);
}

throwAfterTimeout();
throwInSetTimeout();
promiseCatchAfterTimeout();

image.png
image.png
image.png

async関数でsetTimeoutをラップ

const asyncTimeout = async (ms, errorOrValue) => {
  return new Promise((resolve, reject) => {
    if(errorOrValue instanceof Error) setTimeout(() => {reject(errorOrValue)}, ms);
    setTimeout(() => {resolve(errorOrValue);}, ms);
  });
}

asyncTimeout(4000, 'delayed resolve in async timeout')
.then(console.log) //executed
.catch(console.error);

asyncTimeout(4000, new Error('delayed error in async timeout'))
.then(console.log)
.catch(console.error); //executed

image.png

雑記

2020-12-27 公開日

この記事は自分自身へのクイックドキュメントとして存在しています。
一方で、気になるエラーや載せてほしい例があれば

  • 記事へのコメント
  • 私のtwitter: satsukizzzへのアクション(リプライ等) ... Qiita同様スチームミルクを落とす画像
  • N予備校slackのsatsukizzzへのアクション(メンション等) ... ピーチティーの画像
    をしてもらえれば追加します。そのときは、皆さんのクイックドキュメントになればなと思います。
    詳しい方は用語の勘違いの指摘などしていただけると大変ありがたいです。

年末のやっつけで2か月前に試したものを一つ一つ切り出しただけであるため、後々整理すると思われますが、閲覧者の疑問点に合わせて分類するのもわかりやすいかな?と思うので、まずは公開しました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?