現在42Tokyoで最後のチーム課題(Webアプリ開発)に取り組んでいます。
JavaScript/TypeScriptで非同期処理を書くとき、なんとなくasync/awaitで書いてるけど
なぜこの書き方なんだろう?、とふと疑問に思ったので、その歴史を辿ってみました。
※JS視点で書いています。お手柔らかに。。。
非同期処理の進化史:コールバックからasync/awaitへ
JavaScriptにおいて、時間のかかる処理(API通信やタイマーなど)をどう扱うかは常に大きな課題でした。その進化の歴史を、コードの変遷とともに振り返ります。
歴史の流れ
| 世代 | 手法 | 進化のポイント |
|---|---|---|
| 第1世代 | コールバック | 全てを手動で管理(ミスが起きやすい) |
| 第2世代 |
Promise (ES6/2015) |
非同期処理を「オブジェクト」として扱えるようになり、標準化された |
| 第3世代 |
async/await (ES2017) |
Promiseをベースに、さらに「見た目」を同期処理に近づけて完成形へ |
1. コールバック時代 (黎明期)
初期のJavaScriptでは、関数の中に「終わったら実行してほしい関数(コールバック)」を引数として渡すのが唯一の方法でした。
特徴
- 関数を呼び出す際に、その後の予定も一緒に預ける。
- 処理が連続すると、右にどんどんズレていく「コールバック地獄」が発生する。
エラーハンドリングの欠如
- 重複 :
failureCallbackを何度も書かなければならず、コードが冗長になってしまう。 - 見落とし : どこか一箇所でも
failureCallbackを渡し忘れると、そこでエラーが起きても何も起きない(握りつぶされる)危険がある。 - 可読性 : どんどん右側にズレるため、どこがどの処理の終わりなのか分からなくなる。
// 右へ右へと深くなるネスト
getData(function(a) {
getMoreData(a, function(b) {
getEvenMoreData(b, function(c) {
console.log(c);
}, failureCallback);
}, failureCallback);
}, failureCallback);
2. Promiseの登場 (2015年 / ES6)
コールバックの欠点を解決するために、「将来の結果を約束するオブジェクト」としてPromiseが登場しました。
特徴
- 状態の管理 : Pending (待機) , Fulfilled (成功) , Rejected (失敗) の3状態を持つ。
- メソッドチェーン :
.then()で処理を縦につなげられる。 - エラーの一括管理 : 最後に
.catch()を書くだけで、道中で起きたエラーも捕まえられる。
// 縦に流れる「チェーン」構造
getData()
.then(a => getMoreData(a))
.then(b => getEvenMoreData(b))
.then(c => console.log(c))
.catch(error => console.error(error)); // ← これ一つで、全ステップの失敗をキャッチできる!
- 一括管理 : 一度成功したら、後から失敗に変わることはないのでどこで失敗しても、最後の
.catchが拾ってくれる。 - 平坦 :
.then()をつなげるだけで、非同期処理を1つのパイプラインのように連結できるようになった。コールバックのように階段状にならず、上から下へ流れるように読める。
3. async/awaitの登場 (ES2017)
-
async/awaitはJavaScriptが独自に生み出したものではなく、C# 5.0 (2012年) で先に導入された概念をモデルにしている。 -
Promiseをベースに、さらに「同期処理(上から下へ順番に動く普通のコード)」と同じ見た目で書けるようになった。 - 「成功したら次へ、失敗したら
catchへ」という流れが、普通の if 文やエラー処理と同じ感覚で書けるようになる。
| 比較項目 | C# | JavaScript |
|---|---|---|
| 戻り値の型 |
Task または Task<T>
|
Promise |
| キーワード | async/await |
async/await |
| エラー処理 |
try-catch が使える |
try-catch が使える |
どちらの言語も、コンパイラやエンジンが裏側で状態マシン(State Machine)を作成し、処理を一時中断・再開できるように制御している。これにより、スレッド(JSの場合はメインスレッド)をブロックせずに待機が可能になる。
特徴
-
async: 非同期関数であることを宣言する。 -
await:Promiseの結果が返るまでその行で待機する(その間、実行スレッドをブロックしない)。 - 究極の読みやすさ : 非同期であることを意識せずにコードが書ける。
async function executeTasks() {
try {
const result = await doSomething();
const newResult = await doSomethingElse(result);
const finalResult = await doThirdThing(newResult);
console.log(`最終結果: ${finalResult}`);
} catch (error) {
// どの await で失敗しても、ここに来る
failureCallback(error);
}
}
現代では、特別な理由がない限りコールバックを直接ガリガリ書くことは減り、「Promiseを返す関数を、async/awaitでスマートに呼び出す」のが標準スタイルになっている。
まとめ : なぜ進化したのか?
| 時代 | 手法 | 主な解決策 |
|---|---|---|
| ~2014年 | コールバック | 非同期処理の基本。ただしネストが深く制御が困難。 |
| 2015年~ | Promise |
Promiseが明示的に解決。オブジェクトとして結果を扱えるように。 |
| 2017年~ | async/await |
「可読性」を解決。同期処理と同じ感覚で書けるように。 |
私も正直調べるまでは深く考えずに非同期処理はasync/awaitで書くかーと思っていましたが、調べてみるとコールバック地獄の理由やなぜasync/awaitが優れているかが理解できました!
また、Promiseオブジェクトについてももっと知りたくなったので、内容がまとまったらそっちの記事も書きたいと思います!
最後まで読んでいただきありがとうございます!💪