はじめに
今回の記事では、多くの人が一度はつまずいたり、挫折した経験のある「非同期処理」について解説します。
「非同期処理」と聞いて、「うわ、無理…」「なんか苦手…」と感じる人も少なくないのではないでしょうか?
実は、僕自身もこの分野がめちゃくちゃ苦手でした。
プログラミングの処理は基本的に、上から下へ順番に実行されていきます。しかし、非同期処理では処理の途中で他の処理を実行し、非同期のタスクが完了したタイミングでその結果を使って画面を描画したり、データベースにアクセスしたりします。
たとえば画像表示サイトでは、ユーザー情報の取得を待たずに、先に画面操作ができるようになることがあります。
非同期処理は、まさにそういった仕組みを実現するために使われています。
でも正直に言うと、初めて非同期処理を学んだときは、本当に意味がわかりませんでした。
頭の中をミキサーでぐちゃぐちゃにされたような感覚で、「なんでこうなるの?」と何度も混乱しました。
それでも、何度もつまずきながら試行錯誤を繰り返し、ようやく非同期処理の考え方と仕組みが理解できるようになりました。
この記事では、そんな僕がついに「腹落ちした」非同期処理の本質について、できるだけわかりやすくまとめました。
また、この記事には簡単な練習問題も用意していますので、
読むにあたって、以下の2つの準備をおすすめします:
- 紙とペン、そして VS Code や Cursor などのエディターを用意しましょう
 実際にコードを書きながら、一つひとつの動きを確認して進めてみてください。
- 紙に図を書いてみましょう
 後ほど紹介する図を、自分の手で書いてみましょう。非同期処理のタスクがどこで管理され、どの順番で実行されるのかを可視化することで、理解度が一気に深まります。
この2つを実行すれば、記事を読み終えるころには、非同期処理への苦手意識がなくなっているはずです。
「騙されたと思って」ぜひ実践してみてください!
この記事を読むことで得られること
実際に手を動かしてコードを書いたり、処理順番を図にしたりすることで理解度倍増!
JavaScriptの難関、非同期処理について処理を視覚的に確認することができ、
非同期処理への苦手意識がなくなります。
目次
- 同期処理について
- 非同期処理について
- 非同期処理コールバック
- コールバック地獄
- async/awaitとPromiseの比較
- trycatchで例外処理
- マクロタスクとマイクロタスク
- 非同期の並列処理
同期処理について
- 
メインスレッドでコードが順番に実行される処理 - 同期処理では一つの処理が完了するまで次の処理は実行されない
 function sleep(ms) { const startTime = new Date(); while (new Date() - startTime < ms); console.log('sleep done'); } const btn = document.querySelector('button'); btn.addEventListener('click', function(){ console.log('button clicked'); }); sleep(3000)- 
sleep関数は現在の時刻ー関数実行した時の時刻(startTime)を引数に取ったms(ミリ秒)間の差になるまでメインスレッドを占有する処理
- 
sleepの実行が終わらない限り、次の処理(console.log('button clicked');)は行われない
 
非同期処理について
- 
メインスレッドから処理を一時的に切り離されて実行される - メインスレッドはコールスタックで管理され、メインスレッドから切り離された非同期処理はタスクキューで管理されている
- 非同期処理ではメインスレッドから処理が切り離されているのでメインスレッドでの処理は継続される
 
- 
タスクキュー(マクロタスク・マイクロタスク)で処理する順番を管理している(先入れ先出し) - コールスタックとイベントループと連携している
- イベントループは定期的にコールスタックにコンテキストが積まれていないかを確認する仕組み
- コールスタックに処理が積まれていない場合にタスクキューからコールスタックに処理を移行し実行する
 
 function sleep(ms) { const startTime = new Date(); while (new Date() - startTime < ms); console.log('sleep done'); } const btn = document.querySelector('button'); btn.addEventListener('click', function(){ console.log('button clicked'); }); setTimeout(function (){ sleep(3000) ,2000})- 
setTimeoutは非同期処理でメインスレッドから切り離されている。関数実行(ページ読み込み)から2000(2秒間)経ってsleep(3000)が実行される
- メインスレッドから切り離されている時間(2秒間)はメインスレッドでの後続の処理が可能
 
- コールスタックとイベントループと連携している
代表的な非同期処理
非同期処理API
- setTimeout
- Promise
- queueMicrotask
etc…
UIイベント
- クリック
etc…
NWイベント
- HTTPリクエストの送受信
- ソケット通信(WebSocketなど)
…etc
I/Oイベント
- ファイル読み書き(fs.readFileなど)
- データベースとの通信
- 標準入力や出力(ターミナルとのやりとり)
…etc
コールスタック、イベントループ、タスクキュー関係図
いろいろと用語が出てきて、「結局どう関係してるの?」と混乱しているかもしれません。
コールスタック?イベントループ?タスクキュー?
言葉だけだと関係性のイメージがつかみにくいですよね。
最初は難しく感じるかもしれませんが、図に描くことで処理の流れが目に見えるようになり、理解が一気に深まります。
※この段階では「ジョブキュー」と「タスクキュー」の違いは気にしなくてOKです。
どちらもまとめて「タスクキュー」として考えて進めていきましょう。
非同期処理コールバック
function a() {
  setTimeout(function task1() { 
    console.log('task1 done');
  });
  console.log('fn a done');
}
function b() {
  console.log('fn b done');
}
a();
b();
//コンソール出力
'fn a done'
'fn b done'
'task1 done'
練習問題
先程ののコードでは、a()を実行したあとにb()を実行していますが、
実行結果としては b() が task1 より先に表示されてしまいます。
では、「task1が完了したあとに関数bを実行する」には、どのようにコードを書き換えれば良いでしょうか?
先ほど紹介した関係図に書き込むと視覚的に理解できますよ!
回答
function a() {
  setTimeout(function task1() { 
    console.log('task1 done');
    b();
  });
  console.log('fn a done');
}
function b() {
  console.log('fn b done');
}
a();
コードの流れ
関数aを実行(コールスタックに関数aの関数コンテキストとグローバルコンテキストが積まれる)
→ タスクキューに関数をtask1を追加
→ コールスタックから関数aの関数コンテキストが消滅 
→ コールスタックからグローバルコンテキストが消滅 
→ タスクキューから関数task1がコールスタックに移行 
→ 関数task1が実行 
→ 関数 bを実行(コールスタックに関数bの関数コンテキストが積まれる) 
→ 関数bが実行
上記のように非同期とコールバックの仕組みを使うことで関数の順序を変えることができる
コールバック地獄
function sleep(callback, val) {
    setTimeout(function () {
        console.log(val++)
        callback(val)
    }, 1000)
}
sleep(function (val) {
    sleep(function (val) {
        sleep(function (val) {
            sleep(function (val) {
            }, val)
        }, val)
    }, val)
}, 0)
コードの流れ
sleep
- 
callbackとvalを引数に取る
- 
setTimeoutを使い1秒後、コンソールにvalを表示して表示した後にプラス1される
- プラス1されて値がcallbackの引数に渡され処理される
sleep使用側
- 
sleepの1回目は、引数0を受け取り、1秒後にconsole.log(val++)で0を表示。その後、valは1にインクリメントされ、次のcallbackに1が渡される。この時点では、渡された1はまだコンソールには表示されない(次のsleep内で表示される)。
- 
sleepの2回目は、前のコールバックから受け取ったval = 1を引数にして呼び出され、1秒後にconsole.log(val++)で1を表示。その後、valは2になり、次のcallbackに2が渡される。この時点でも、2はまだ表示されない。
以下、繰り返し
出力結果
| 時間経過 | val | console.log(val++)の出力 | 次に渡すval | 
|---|---|---|---|
| 0秒 | - | - | sleep(0) 開始 | 
| 1秒後 | 0 | 0 | 1 | 
| 2秒後 | 1 | 1 | 2 | 
| 3秒後 | 2 | 2 | 3 | 
| 4秒後 | 3 | 3 | 4 | 
このコードは正常に動作しますが、コールバック関数のネストが深くなり、コードの可読性や保守性が著しく低下してしまいます。
このようにコールバックを連ねて非同期処理をつなげる方法は「コールバック地獄」と呼ばれ、推奨されません。
そこで、JavaScriptのPromiseやasync/awaitを使うことで、よりシンプルで読みやすい非同期処理の記述が可能になります。
次のセクションでは、同じ処理をPromiseとasync/awaitで書き直してみましょう。
Promise
- 非同期処理をより簡単に、可読性が上がるように書けるようにしたもの
コールバック地獄の解消
function sleep(val) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(val++);
      resolve(val);
    }, 1000);
  })
};
sleep(0)
.then(val => sleep(val))
.then(val => sleep(val))
.then(val => sleep(val));
コードの流れ
sleep関数
- 引数に valを受け取る
- 
new Promiseにより Promise インスタンスを生成- 
Promiseコンストラクタのコールバック関数は、resolveとrejectという2つの引数を取る(今回はresolveのみ使用)
 
- 
- 
setTimeoutを使って1秒後に以下の処理を行う:- 
console.log(val++)で現在のvalを表示し、その後valをインクリメント
- 
resolve(val)を呼び出し、次の.then()に値を渡す
 
- 
- 
Promiseインスタンスを返すことで、.then()で非同期処理をつなげることができる
sleep使用側
- 
sleep(0)を最初に呼び出し、.then()を使ってその後の処理をチェーンさせていく
- 各 .then()の中では次のsleep(val)を呼び出して return することで、処理の流れが順番に続いていく
- 
.then()に渡された関数は、前のresolve(val)の結果を引数として受け取る
- 
.then()に処理をつなげるには、常にPromiseを返す必要がある。- 
resolve(val)→.then(val => ...)
- 
reject(err)→.catch(err => ...)に値が渡される
 
- 
.then/.catch で処理をつなげるために、必ず戻り値にPromiseのインスタンスを返す
- 
resolve→then
- 
reject→catch
async/await
Promiseをより直感的に書けるようにしたもの
// async 関数を変数に代入するパターン
const fetchData = async (url) => {
  const response = await fetch(url);
  // 後続の処理を書く
};
// 通常の関数宣言パターン
async function fetchData(url) {
  const response = await fetch(url);
  // 後続の処理を書く
}
コードの流れ
- 
async関数の前に asyncを付けることで、その関数を非同期関数(Async Function)として定義できる。- 
async関数は常にPromiseを返す(戻り値を自動的に Promise にラップする)
- 
Promiseが解決(resolve)されるまで 処理を待機できる
- 例:return 1と記述しても、実際はPromise.resolve(1)を返す
 
- 
- 
awaitawaitは Promise の完了を待つ演算子。- 
awaitを使うと、Promise が解決されるまで 後続の処理を一時停止する
- 
async関数の中でのみ使用可能- 
asyncでない関数内で使用すると エラーになる
 
- 
- 一般的に、async関数とawaitは セットで使われる
 
- 
async/awaitとPromiseの比較
function sleep(val) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      console.log(val++);
      resolve(val);
    }, 1000);
  })
};
async function init (){
  let val = await sleep(0);
  val = await sleep(val)
  val = await sleep(val)
  val = await sleep(val)
  // この return は実行されません(上の throw により中断される)
  console.log(val)
} 
init();
同じ処理を Promise チェーンで書いた場合
sleep(0)
.then(val => sleep(val))
.then(val => sleep(val))
.then(val => sleep(val));
- 上記の async/awaitの実装は、Promise セクションで紹介した処理と 同じ挙動
- 
awaitを使うとresolve(val)の引数valが、そのままinit関数内のvalに代入される- 一方、Promise では .then(val => ...)を使って 明示的に受け渡す必要
 
- 一方、Promise では 
- 両者を比較すると、async/awaitを使う方が よりシンプルで読みやすく、直感的に記述できる
- なお、awaitを付けずに呼び出すと、戻り値は Promise オブジェクトになる
更に関数init(async関数)は Promiseを返しますのでthenやcatchで処理をつなげられる
//・・・上略(sleep関数)
async function init (){
  let val = await sleep(0);
  val = await sleep(val)
  val = await sleep(val)
  val = await sleep(val)
  throw new Error ('not Data')
  return val
} 
init().then(function (data) {
  console.log(data);
}).catch(function(e) {
  console.error (e)
})
- 3回目のawait sleep(val)の後、throw new Error ('not Data')が実行されエラーを出力される
- この場合valは返されず、エラーが発生してcatchブロックに処理が移る
- このように、async関数内で発生した例外は 自動的に Promise の reject として扱われ、.catch()で補足可能
try&catchで例外処理
コードの流れ
throwがある場合
try {
    console.log("start")
    throw new Error ("No Data")
    console.log("task")
} catch (e) {
    console.error(e)
} finally {
    console.log("end")
}
- 
tryブロック内のconsole.log("start")がまず実行される
- 次に throw new Error("No Data")により例外が発生し、その時点でtryブロックの処理は中断される- よって、console.log("task")は実行されない
 
- よって、
- 発生したエラーは catch (e)に渡され、変数eに格納される- 
e.messageには"No Data"が格納されており、console.error(e)によってエラーオブジェクトが出力される
 
- 
- 最後に finallyブロックが実行され、console.log("end")によって"end"が出力される- 
finallyは、エラーの有無に関わらず必ず実行されるブロック
 
- 
throwがない場合
try {
  console.log("start");
  console.log("task");
} catch (e) {
  console.error(e);
} finally {
  console.log("end");
}
- 
tryブロック内の処理はすべて正常に実行される- 
console.log("start")とconsole.log("task")の両方が出力される
 
- 
- エラーが発生しないため、catchブロックはスキップされる
- 最後に finallyブロックが実行され、console.log("end")が出力される
try&cachを使った例外処理
async function fetchData() {
    const res = await fetch("users.json")
    if (res.ok) {
        const json = await res.json()
        if (json.length === 0) {
            throw new Error('No Data')
        }
        return json
    }
}
const userData = async function () {
    try {
        const users = await fetchData()
        for (const user of users) {
            console.log(`I'm ${user.name}(${user.age})`)
        }
    } catch (e) {
        console.error(e)
    } finally {
        console.log("end")
    }
}
userData();
コードの流れ
fetchData(async関数)
- この関数では、users.jsonというファイルからデータを取得しているが、本来は API やデータベースから取得する想定
- 非同期で users.jsonファイルからデータを取得する
- fetch によって返された Response オブジェクトの okプロパティがtrueの場合、レスポンスを.json()メソッドで JavaScript の配列に変換する- 取得した配列が空(json.length === 0)だった場合- 
throw new Error('No Data')によってエラーを投げ、catchブロックに処理が移る
 
- 
- 配列にデータがある場合
- 配列データ(json)を返す
 
- 配列データ(
 
- 取得した配列が空(
userData(async関数)
- 
tryブロック(正常にデータが取得できた場合)では- 
fetchData()の戻り値をusers変数に格納
- 
for...ofループでusersを順番に処理し、console.log(I'm ${user.name}(${user.age}))で各ユーザーの名前と年齢を出力
 
- 
- 
catchブロック(throw new Error()によってエラーが投げられた場合)では- エラーオブジェクト eをconsole.error(e)で表示
 
- エラーオブジェクト 
- 
finallyブロックでは- 
tryまたはcatchの処理が終了した後、必ずconsole.log("end")を実行して"end"を出力する
 
- 
カスタムエラー
try&cachを使った例外処理のコードに下記を追加することでカスタムエラーの作成ができる
class NoDataError extends Error {
    constructor(message) {
        super(message) 
        this.name = NoDataError
    }
}
コードの流れ
- 
NoDataErrorというクラスを、組み込みのErrorクラスを継承して定義している
- このクラスは、インスタンス化される際に messageを引数として受け取る
- 
super(message)は親クラス(ここではErrorクラス)のコンストラクタを呼び出すもので、エラーメッセージの内容をErrorオブジェクトとして正しく扱えるようにする
- 
this.name = 'NoDataError'によって、エラーオブジェクトのnameプロパティが"NoDataError"に設定され、エラー出力時にカスタムエラー名が表示されるようになる
使用例
throw new NoDataError('not Data')
const userData = async function () {
    try {
        const users = await fetchData()
        for (const user of users) {
            console.log(`I'm ${user.name}(${user.age})`)
        }
    } catch (e) {
	    if( e instanceof NoDataError) {
        console.error(e)
       } else {
		    console.error("error!!!")
       }
    } finally {
        console.log("end")
    }
}
- 
Errorを継承したNoDataError('not Data')でエラーを投げてcatchブロックに処理を移行する
- 
catchブロック内では、発生したエラーがどのクラス(インスタンス)から投げられたかを判定し、それに応じて処理を分岐している
- エラーが NoDataErrorのインスタンスであれば、そのエラーオブジェクトをconsole.error(e)で出力
- 
NoDataError以外のエラー(たとえばTypeErrorやReferenceErrorなど)の場合は、"error!!!"という文字列を出力する
マクロタスクとマイクロタスク
マイクロタスク
マクロタスクよりも優先して実行される
例: Promise、queueMicrotask など
マクロタスク
マイクロタスクがすべて完了してから実行される
例: setTimeout、setInterval など
- 処理の途中で新たにマイクロタスクが追加された場合でも、マイクロタスクがすべて完了するまでマクロタスクは実行されない
ここでもう一度先ほどの関係図を見るとマクロタスク(タスクキュー)、マイクロタスク(ジョブキュー)についてもイメージができると思います。

new Promise(function promise(resolve) {
  console.log('promise');
  setTimeout(function task1() {
    console.log('task1');
    resolve();
  });
}).then(function job1() {
  console.log('job1');
  setTimeout(function task1() {
    console.log('task2');
    queueMicrotask(function job4() {
      console.log('job4')
    })
  });
  
}).then(function job2() {
  console.log('job2');
}).then(function job3() {
  console.log('job3');
})
console.log('global end');
//コンソール出力
promise
global end
task1
job1
job2
job3
task2
job4
コードの流れ(イベントループとタスクの順序)
ここで同期タスク終了 → マクロタスクの処理へ移る
- 
task1実行 →console.log('task1')→task1
- 
resolve()呼び出し → Promise 解決 →.then(job1)登録 → マイクロタスク1
マイクロタスク処理開始
- 
job1実行 →console.log('job1')→job1
- 
setTimeout(task2)登録 → マクロタスク2
- 
.then(job2)登録 → マイクロタスク2
- 
.then(job3)登録 → マイクロタスク3
マイクロタスク継続処理
- 
job2実行 →console.log('job2')→job2
- 
job3実行 →console.log('job3')→job3
ここでマイクロタスクが空 → 次のマクロタスク(task2)へ移る
- 
task2実行 →console.log('task2')→task2
- 
queueMicrotask(job4)登録 → マイクロタスク4
マクロタスク終了 → マイクロタスク実行
- 
job4実行 →console.log('job4')→job4
練習問題-2
console.log('start');
setTimeout(() => {
  console.log('timeout1');
  Promise.resolve().then(() => {
    console.log('micro1');
  });
  setTimeout(() => {
    console.log('timeout2');
  });
}, 0);
Promise.resolve().then(() => {
  console.log('micro2');
  return Promise.resolve().then(() => {
    console.log('micro3');
  });
}).then(() => {
  console.log('micro4');
});
queueMicrotask(() => {
  console.log('micro5');
});
console.log('end');
上記のコードがどのように動き、どの順番でコンソールに出力されるか考えてみて下さい。
答えが決まったらスクロールして下さい。
回答
//コンソール出力
start  
end  
micro5  
micro2  
micro3  
micro4  
timeout1  
micro1  
timeout2
コードの流れ(イベントループとタスクの順序)
- 
console.log('start')→ 同期 →start
- 
setTimeout(..., 0)登録 → マクロタスク1
- 
Promise.resolve().then(...)登録 → マイクロタスク1- 
micro2, さらに.then(() => micro3)→ ネスト → マイクロタスク2
 
- 
- 
.then(() => micro4)→ マイクロタスク3(micro3のあと)
- 
queueMicrotask(...)登録 → マイクロタスク4(micro5)
- 
console.log('end')→ 同期 →end
ここで同期タスク終了 → マイクロタスクの処理へ移る
- 
micro5(queueMicrotask) →micro5
- 
micro2→micro2
- 
micro3(micro2の中)→micro3
- 
micro4(micro3のあと)→micro4
ここでマイクロタスクが空 → 次のマクロタスク(setTimeout)
- 
timeout1→timeout1
- 
Promise.resolve().then(...)→ マイクロタスク登録(micro1)
- 
setTimeout(...)→ マクロタスク登録(timeout2)
マクロタスク内のマイクロタスク実行
- micro1
- timeout2
非同期の並列処理
- 
Promiseを直列につなぐ処理は「Promiseチェーン」と呼ばれる
- 
一方、複数のPromiseを同時に実行するのが「並列処理」 Promise.all()やPromise.race()を使って複数の非同期処理を同時に扱えます。
Promise.all(反復可能オブジェクト)
function sleep(val) {
  return new Promise(function (resolve,reject) {
    setTimeout(function () {
      console.log(val++);
      resolve(val);
    }, val * 1000);
  });
}
Promise.all([sleep(1), sleep(2), sleep(3)])
	.then(function(data) {
   console.log(data);
})
//コンソール出力
1
2
3
(3) [2, 3, 4]
コードの流れ
sleep関数
- 
sleep(val)はPromiseを返す関数
- 
setTimeoutを使ってval秒後に処理を実行する(例:sleep(2)なら2秒後)
- 
console.log(val++)によって現在の値を出力し、次に使うためにインクリメント
- 
resolve(val)により、インクリメントされた値を次の.then()に渡す
Promise.all()
- 
Promise.all([...])は、配列内のすべての Promise が「成功」するのを待ってから次の.then()に進む
- すべてが完了すると、各 resolveの値が配列となって渡される
Promiseチェーンの戻り値にPromise.all([...])を渡した場合
sleep(1).then(function(val) {
  return (Promise.all([sleep(2), sleep(3), sleep(4)]))
}).then(function(val) {
  console.log(val)
  return sleep(val[1]);
}).then(function(val) {
  return sleep(val);
})
//コンソール出力
1
-------
2   ↑
3   並列
4   ↓
-------
(3) [3, 4, 5]
4
5
コードの流れ
- 
sleep(1)により 1秒後に1を出力、resolve(val)には 2 が渡る
- 
.then()でPromise.all([sleep(2), sleep(3), sleep(4)])を返す:- この3つの sleep()は並列でスタート
- それぞれ 2s,3s,4s後に2,3,4を出力(インクリメントされるので[3, 4, 5]を返す)
 
- この3つの 
- 
.then()に渡されるvalは[3, 4, 5]。その配列のval[1]=4を次のsleep()に渡す
- 
sleep(4)→ 4秒後に4を出力、val++ →5を返す
- 
sleep(5)→ 5秒後に5を出力
補足
- 
Promise.all([...])で並列処理- 配列の各要素は Promiseを返す必要がある。
- すでに解決済みの値[1,2,3](同期処理)を渡しても内部で Promise.resolve()に包まれる(Promise.all([1, 2, 3])はPromise.all([Promise.resolve(1), ...])と同じ動作になる)ため.then()は実行される。ただし、Promise.all()の本来の目的は非同期処理の並列実行なので、通常は非同期関数のPromiseを渡す
 
- 配列の各要素は 
- 
sleep().then()のようにつなぐと直列処理
- 複雑なフローでも .then()チェーンをうまく使えば、直列→並列→直列 のような処理が実現できる
Promise.race(反復可能オブジェクト)
//・・・sleep関数、ボタンイベントは省略
Promise.race([sleep(1),sleep(2),sleep(3)])
.then(function (data) {
  console.log(data)
})
//コンソール出力
1
2
2
3
コードの流れ
Promise.race()
- 
Promise.race([...])は、配列内で一番最初に完了(解決または拒否)した Promise の結果に応じて、次の.then()または.catch()に進む
- 一番早く完了した Promiseの**結果(値またはエラー)**が、.then()または.catch()に渡される
Promise.allSettled(反復可能オブジェクト)
//shouldReject = false/trueでresolve/rejectを切り替え
function sleep(val, shouldReject = false) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(val++);
      if (shouldReject) {
        resolve(val);
      } else {
        reject(val);
      }
    }, val * 1000);
  });
}
Promise.allSettled([sleep(1),sleep(2),sleep(3)])
.then(function (data) {
  console.log(data)
}).catch(function(e) {
  console.error(e)
})
//コンソール出力 
resolve(val)
1
2
3
(3) [{…}, {…}, {…}]
{status:'fulfilled', value:2}
{status:'fulfilled', value:2}
{status:'fulfilled', value:3}
reject(val)
1
2
3
(3) [{…}, {…}, {…}]
{status:'rejected', reason:2}
{status:'rejected', reason:2}
{status:'rejected', reason:3}
コードの流れ
Promise.allSettled()
- 
Promise.allSettled([...])は、配列内のすべての Promise が完了(成功・失敗どちらでも)するのを待ち、必ず次の.then()に進む
- すべての処理が終わると、結果は各 Promise の状態を表すオブジェクトの配列で返される
- 各オブジェクトは成功時、 { status: 'fulfilled', value: 結果 }、失敗時、{ status: 'rejected', reason: エラー内容 } の形になっているため、status プロパティを確認して成功・失敗を判別する
最後に
いかがだったでしょうか?
この記事を読む前よりも、非同期処理について少しでも理解が深まったと感じてもらえたら嬉しいです。
すぐ完璧に理解できなくても大丈夫。
非同期処理は一度でスッと理解できるものではありません。繰り返し手を動かしながら学習することで、必ず身についていきます。
実を言うと、僕自身も2年前まではPC操作すらままならず、最近まで非同期処理が苦手すぎて「なんとなく避けて」きました。
でも、何度もつまずきながらも少しずつ向き合っていくうちに、やっと「わかった!」と思える瞬間がやってきました。
非同期処理が分かるようになると、JavaScriptでの開発がぐっと楽しくなります。
「苦手だな…」と感じていたことが「おもしろい!」に変わる瞬間を、ぜひ体験してみてください。
もし、もっとJavaScriptの本質や仕組みを深く学びたいと思った方は、参考書籍や教材にチャレンジしてみるのもおすすめです。
(ただし、ただ、決して難易度は低くないです。焦らずじっくり取り組んでくださいね!)