135
158

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【超有料級】非同期処理がスッと理解できる魔法のレッスン。今日からあなたも非同期の魔法使い!

Posted at

はじめに

今回の記事では、多くの人が一度はつまずいたり、挫折した経験のある「非同期処理」について解説します。

「非同期処理」と聞いて、「うわ、無理…」「なんか苦手…」と感じる人も少なくないのではないでしょうか?
実は、僕自身もこの分野がめちゃくちゃ苦手でした。
プログラミングの処理は基本的に、上から下へ順番に実行されていきます。しかし、非同期処理では処理の途中で他の処理を実行し、非同期のタスクが完了したタイミングでその結果を使って画面を描画したり、データベースにアクセスしたりします。
たとえば画像表示サイトでは、ユーザー情報の取得を待たずに、先に画面操作ができるようになることがあります。
非同期処理は、まさにそういった仕組みを実現するために使われています。

でも正直に言うと、初めて非同期処理を学んだときは、本当に意味がわかりませんでした。
頭の中をミキサーでぐちゃぐちゃにされたような感覚で、「なんでこうなるの?」と何度も混乱しました。
それでも、何度もつまずきながら試行錯誤を繰り返し、ようやく非同期処理の考え方と仕組みが理解できるようになりました。

この記事では、そんな僕がついに「腹落ちした」非同期処理の本質について、できるだけわかりやすくまとめました。

また、この記事には簡単な練習問題も用意していますので、
読むにあたって、以下の2つの準備をおすすめします:

  • 紙とペン、そして VS Code や Cursor などのエディターを用意しましょう
     実際にコードを書きながら、一つひとつの動きを確認して進めてみてください。
  • 紙に図を書いてみましょう
     後ほど紹介する図を、自分の手で書いてみましょう。非同期処理のタスクがどこで管理され、どの順番で実行されるのかを可視化することで、理解度が一気に深まります。

この2つを実行すれば、記事を読み終えるころには、非同期処理への苦手意識がなくなっているはずです。
「騙されたと思って」ぜひ実践してみてください!

この記事を読むことで得られること

実際に手を動かしてコードを書いたり、処理順番を図にしたりすることで理解度倍増!
JavaScriptの難関、非同期処理について処理を視覚的に確認することができ、
非同期処理への苦手意識がなくなります。

目次

同期処理について

  • メインスレッドでコードが順番に実行される処理

    • 同期処理では一つの処理が完了するまで次の処理は実行されない
    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

コールスタック、イベントループ、タスクキュー関係図

いろいろと用語が出てきて、「結局どう関係してるの?」と混乱しているかもしれません。
コールスタック?イベントループ?タスクキュー?
言葉だけだと関係性のイメージがつかみにくいですよね。

そんなときは、ぜひ以下の図を紙に手で書いてみてください。
Image from Gyazo

最初は難しく感じるかもしれませんが、図に描くことで処理の流れが目に見えるようになり、理解が一気に深まります。

※この段階では「ジョブキュー」と「タスクキュー」の違いは気にしなくて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

  • callbackvalを引数に取る
  • setTimeout を使い1秒後、コンソールにvalを表示して表示した後にプラス1される
  • プラス1されて値がcallbackの引数に渡され処理される

sleep使用側

  • sleep の1回目は、引数 0 を受け取り、1秒後に console.log(val++)0 を表示。その後、val1 にインクリメントされ、次の callback1 が渡される。この時点では、渡された 1 はまだコンソールには表示されない(次の sleep 内で表示される)。
  • sleep の2回目は、前のコールバックから受け取った val = 1 を引数にして呼び出され、1秒後に console.log(val++)1 を表示。その後、val2 になり、次の callback2 が渡される。この時点でも、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 コンストラクタのコールバック関数は、resolvereject という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のインスタンスを返す

  • resolvethen
  • 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) を返す
  • await

    awaitPromise の完了を待つ演算子

    • 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 => ...) を使って 明示的に受け渡す必要
  • 両者を比較すると、async/await を使う方が よりシンプルで読みやすく、直感的に記述できる
  • なお、await を付けずに呼び出すと、戻り値は Promise オブジェクトになる

更に関数initasync関数)は Promiseを返しますのでthencatchで処理をつなげられる

//・・・上略(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() によってエラーが投げられた場合)では
    • エラーオブジェクト econsole.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 以外のエラー(たとえば TypeErrorReferenceError など)の場合は、"error!!!" という文字列を出力する

マクロタスクとマイクロタスク

マイクロタスク

マクロタスクよりも優先して実行される

例: PromisequeueMicrotask など

マクロタスク

マイクロタスクがすべて完了してから実行される
例: setTimeoutsetInterval など

  • 処理の途中で新たにマイクロタスクが追加された場合でも、マイクロタスクがすべて完了するまでマクロタスクは実行されない

ここでもう一度先ほどの関係図を見るとマクロタスク(タスクキュー)、マイクロタスク(ジョブキュー)についてもイメージができると思います。
Image from Gyazo

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

コードの流れ(イベントループとタスクの順序)

ここで同期タスク終了 → マクロタスクの処理へ移る

  1. task1 実行 → console.log('task1')task1
  2. resolve() 呼び出し → Promise 解決 → .then(job1) 登録 → マイクロタスク1

マイクロタスク処理開始

  1. job1 実行 → console.log('job1')job1
  2. setTimeout(task2) 登録 → マクロタスク2
  3. .then(job2) 登録 → マイクロタスク2
  4. .then(job3) 登録 → マイクロタスク3

マイクロタスク継続処理

  1. job2 実行 → console.log('job2')job2
  2. job3 実行 → console.log('job3')job3

ここでマイクロタスクが空 → 次のマクロタスク(task2)へ移る

  1. task2 実行 → console.log('task2')task2
  2. queueMicrotask(job4) 登録 → マイクロタスク4

マクロタスク終了 → マイクロタスク実行

  1. 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

コードの流れ(イベントループとタスクの順序)

  1. console.log('start') → 同期 → start
  2. setTimeout(..., 0) 登録 → マクロタスク1
  3. Promise.resolve().then(...) 登録 → マイクロタスク1
    • micro2, さらに .then(() => micro3) → ネスト → マイクロタスク2
  4. .then(() => micro4) → マイクロタスク3(micro3のあと)
  5. queueMicrotask(...) 登録 → マイクロタスク4(micro5
  6. console.log('end') → 同期 → end

ここで同期タスク終了 → マイクロタスクの処理へ移る

  1. micro5(queueMicrotask) → micro5
  2. micro2micro2
  3. micro3micro2の中)→ micro3
  4. micro4micro3のあと)→ micro4

ここでマイクロタスクが空 → 次のマクロタスク(setTimeout)

  1. timeout1timeout1
  2. Promise.resolve().then(...) → マイクロタスク登録(micro1
  3. setTimeout(...) → マクロタスク登録(timeout2

マクロタスク内のマイクロタスク実行

  1. micro1
  2. 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

コードの流れ

  1. sleep(1) により 1秒後に 1 を出力、resolve(val) には 2 が渡る
  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() に渡す
  4. sleep(4) → 4秒後に 4 を出力、val++ → 5 を返す
  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の本質や仕組みを深く学びたいと思った方は、参考書籍や教材にチャレンジしてみるのもおすすめです。
(ただし、ただ、決して難易度は低くないです。焦らずじっくり取り組んでくださいね!)

参考教材

【JS】ガチで学びたい人のためのJavaScriptメカニズム

135
158
2

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
135
158

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?