【JavaScript】非同期処理 まとめ
JavaScriptの躓きポイントの代表格である非同期処理(Promise や async/await )について解説します。
非同期処理とは
非同期処理とは、あるタスクを実行をしている際に、他のタスクが別の処理を実行できる方式をいいます。
一般的に、データベースから値を取得する等、通信を伴う処理は通信状況によって取得まで時間がかかったり、値が返ってくる保証がない等、時間がかかる処理とされています。
このような時間のかかる処理を、シングルスレッドで実行すると、実行中は他のタスクを行うことができなくなります。
例えば、データベースから値を取得する処理が走った後に、ユーザがページスクロールをしたとします。その場合、DBから値を取得し終わるまで、ページはスクロールされず、ユーザからすると画面がフリーズしたと思われるかもしれません。
このような事態を防ぐため、非同期処理が存在します。
先ほどのページスクロールの例を図に表すと以下のようになります。
同期処理の場合は、ページスクロールをしてから実際にスクロールされるまでに待ち時間が発生していましたが、非同期処理ではDBから値を取得するタスクを一時中断して先にページスクロールを完了させた後、再度DBから値取得を行っています。
シングルスレッドでこれを実現させるには、何らかの非同期処理がきた場合は、その処理の完了を待たずに、次のタスクを実行する必要があります。
完了を待っていたら割込みタスクを実行できませんからね。
JavaScriptで非同期処理を実装する上で、非同期処理は処理の完了を待たないという特徴がとても大切になってくるのでここで覚えておきましょう。
JavaScript の非同期処理について
JavaScriptは歴史のある言語で、いくつものアップデートがされてきました。中でも、ES2015というバージョンからは大きな変化があり、非同期処理もまたES2015以降から新たな書き方が追加され、2017年にはさらに便利な非同期処理の書き方が追加されています。
ここでは、ES2015で追加されたPromise、ES2017で追加されたasyn/awaitについて解説していきます。
※JavaScriptの歴史について調べると、沼にハマることができるので、お時間のある方は一度沼にハマってみると面白いかもしれません。
Promise
Promise の基本理解
Promiseとは、非同期処理を行うためのものです。
まず、Promiseには以下3つの状態があります。
-
pending:非同期処理の実行中の状態を表す -
fulfilled:非同期処理が正常終了した状態を表す -
rejected:非同期処理が異常終了した状態を表す
取り合えずサンプルコードを実行してこれらの存在を確認してみましょう。
まずはPromiseの基本構文から。Promiseは以下のような式になります。
new Promise(function (resolve, reject){
// 非同期処理
})
Promise()内のコールバック関数の引数にはそれぞれ、resolve、rejectが渡ります。
引数はそれぞれ以下の役割を持ちます。
-
resolve:非同期処理が正常終了したことを知らせるメソッド。returnの代わりに、resolve()と記述することで、非同期関数が正常終了したことを知らせる。 -
reject:非同期処理が異常終了したことを知らせるメソッド。returnの代わりに、reject()と記述することで、非同期関数が異常終了したことを知らせる。
上記ソースコード(Promiseの基本構文)をJavaScriptのコンソールで実行すると以下のような結果が得られます。
非同期処理の実行中を表す、pendingが表示されました。
これは、非同期処理が正常終了したことを表すresolveと異常終了したことを表すrejectのいずれも呼び出されていないため、非同期処理が完了していない、ということです。
そのため、非同期処理を呼び出す場合は必ず非同期処理が終了したことを教える必要があります。
非同期処理が完了したことを知らせるためには、// 非同期処理で処理が正常終了したら、returnの代わりにresolve()を実行し、異常終了した場合はreturnの代わりにreject()を実行します。
また、処理が成功したか失敗したかを検知できるように、try, catchで処理を囲みます。
new Promise(function (resolve, reject){
try {
// 非同期処理
// returnの代わりに正常終了したことを表すresolveを呼び出して、fulfilledを返す
resolve();
} catch (e) {
// 異常終了時の処理
// returnの代わりに異常終了したことを表すrejectを呼び出して、rejectedを返す
reject();
}
})
上記ソースコードをJavaScriptのコンソールで実行すると以下のような結果が得られます。
非同期処理が正常終了した状態を表すfulfilledが表示されました。
では、異常終了させた場合はどうなるでしょうか。
new Promise(function (resolve, reject){
try {
// 非同期処理
// 異常終了させる
throw "異常終了";
// returnの代わりに正常終了したことを表すresolveを呼び出して、fulfilledを返す
resolve();
} catch (e) {
// 異常終了時の処理
// returnの代わりに異常終了したことを表すrejectを呼び出して、rejectedを返す
reject();
}
})
非同期処理が異常終了した状態を表すrejectedが表示されました。
実際に非同期処理を走らせてみる
先ほど、非同期処理は処理の完了を待たない、ということをお伝えしました。
Promiseは非同期処理のため、new Promise内のコールバック関数の処理が完了する前に、その後の処理が終了してしまうことになります。
こちらもサンプルコードで処理の流れを確認してみましょう。
new Promise(function (resolve, reject){
try {
// 1秒後に"非同期処理"とコンソールに出力
setTimeout(()=>{
console.log("非同期処理")
// returnの代わりに正常終了したことを表すresolveを呼び出して、fulfilledを返す
resolve()
}, 1000)
} catch (e) {
// 異常終了時の処理
// returnの代わりに異常終了したことを表すrejectを呼び出して、rejectedを返す
reject()
}
})
上記コードを実行すると以下のような実行結果になります。
setTimeout内でresolve()を呼び出しているにもかかわらず、ログにはpendingと表示され、その後"非同期処理"のログが出力されました。
これが非同期処理は処理の完了を待たないという特徴です。非同期処理内で処理の完了を伝えるresolve()やreject()を呼んだとしても、それを待つことなく次の処理に進んでしまいます。
非同期処理の特徴を理解したところで、次は処理の完了の待ち方についてみていきます。
非同期処理の完了を待つには
それでは、どのようにして非同期処理の完了を待てば良いのかについて解説していきます。
Promiseに限らず、JavaScriptの非同期処理は、その戻り値に対してthenメソッドとcatchメソッドが利用できます。
それぞれの意味は以下の通りです。
-
then():非同期処理が正常終了した際に呼ばれるメソッド -
catch():非同期処理が異常終了した際に呼ばれるメソッド
これをみると、先ほど確認したPromiseの3つの状態との関係性が分かると思います。
resolveは非同期処理が正常終了したことを表すもので、then()は非同期処理が正常終了した際に呼ばれるメソッド(関数)です。
つまり、非同期処理が処理中(pending)から、正常終了(resolve)に変化したら、続いてthen()が呼ばれるというです。
catch()はその逆で、非同期処理が処理中(pending)から、異常終了(reject)に変化したら、呼ばれるということになります。
こちらもサンプルコードで処理の流れを確認してみましょう。
function asyncFunction() {
return new Promise((resolve, reject) => {
try {
// 1秒後に"非同期処理"とコンソールに出力
setTimeout(() => {
console.log("非同期処理")
// returnの代わりに正常終了したことを表すresolveを呼び出して、fulfilledを返す
resolve();
}, 1000)
} catch (e) {
// 異常終了時の処理
// returnの代わりに異常終了したことを表すrejectを呼び出して、rejectedを返す
reject();
}
})
}
asyncFunction().then(() => {
console.log("resolve後の処理");
}).catch(e => {
console.log("reject後の処理");
})
上記コードを実行すると以下のような実行結果になります。
setTimeout内の"非同期処理"というログ出力のあとにthen()メソッド内の"resolve後の処理"というログが出力されていることがわかります。
このように、非同期処理の完了後に他の処理を行いたい場合は非同期処理.then().catch()といった形に、メソッドチェーンでthen()とcatch()を続けてやればOKです。
非同期で処理した後に、その値を取得する
非同期処理の完了の待ち方はわかりましたが、非同期処理中に取得した値を活用したい場合はどうすればよいかについて解説していきます。
実はthen()メソッドとcatch()メソッドはそれぞれ引数を取ることができます。
then()とcatch()に値を渡すには、処理を完了したことを知らせるresolve()とreject()で引数を渡すことで、任意の値を送ることができます。
これもサンプルコードを見てみましょう。
function asyncFunction() {
return new Promise((resolve, reject) => {
try {
// 1秒後に"非同期処理"とコンソールに出力
setTimeout(() => {
console.log("非同期処理")
const num = 1
resolve(num);
}, 1000)
} catch (e) {
// 異常終了時の処理
// returnの代わりに異常終了したことを表すrejectを呼び出して、rejectedを返す
reject(e);
}
})
}
asyncFunction().then((num) => {
console.log(`引数で受け取った値:${num}`);
}).catch(e => {
console.log(`引数で受け取った値:${e}`);
})
上記コードを実行すると以下のような実行結果になります。
このように、処理の完了をお知らせするついでに引数に渡したいものを入れることで、then()やcatch()で受け取ることができます。
以上がPromiseの基本的な使い方になります。
ここまでで、理解できていない部分がある方は、先に進まずにゆっくりとPromiseについて理解をしてください。
async / await
async / await の基本理解
それでは、続いてES2017で導入されたasync / awaitについてみていきましょう。
といっても、Promiseを理解したら、async / awaitの理解はそんなに難しくありません。
なぜなら、async / awaitはPromiseの糖衣構文だからです。
糖衣構文とは、
プログラミング言語において、読み書きのしやすさのために導入される書き方であり、複雑でわかりにくい書き方と全く同じ意味になるものを、よりシンプルでわかりやすい書き方で書くことができるもののこと
つまり、Promiseを簡単に書けるようにしたものが、async / awaitということです。
async
まずはasyncについてみていきましょう。
asyncは、非同期関数を作るものです。基本構文は以下の通りです。
function宣言の前にasyncとつけるだけです。
async function asyncFunction() {}
async functionの特徴は以下の通りです。
-
async functionは呼び出されるとPromiseを返す。 async functionが値をreturnした場合はその値をresolveする。async functionが例外をthrowした場合はその値をrejectする。
ここでPromiseを返す???と思った方、Promiseとは3つの状態を持つオブジェクトのことでしたね。
pending:非同期処理の実行中の状態を表すfulfilled:非同期処理が正常終了した状態を表すreject:非同期処理が異常終了した状態を表す
実行したらpendingとか出力していたあれのことです。
ここで、Promiseとasync functionでどのように書き方が異なるのか、サンプルコードで見てみましょう。
function promiseFunction() {
return new Promise((resolve, reject) => {
try {
resolve("resolve")
} catch (e) {
reject("reject")
}
})
}
// アロー関数で書くとこうなります
// const asyncFunction = async () => {
async function asyncFunction() {
try {
return "resolve"
} catch (e) {
throw "reject"
}
}
どうでしょうか。async functionの方がreturn new Promise()がいらなくなったので、見た目がスッキリしていると思います。
Promiseがなくなったことで、resolve、rejectが引数に取れなくなったので、returnとthrowがその代わりとなっていますね。
await
それでは、続いてawaitについてみていきましょう。
awaitは非同期処理の完了を待つためのものです。
もう少し具体的に説明すると、Promiseの結果(resolveもしくはreject)が返されるまで待機する(処理を一時停止する)演算子のことです。
ただし、ここで重要な注意点として awaitはasync functionの中でしか使えない、という制約があります。
こちらもPromiseとの比較をみていきましょう。
function promiseFunction() {
return new Promise((resolve, reject) => {
try {
resolve("resolve")
} catch (e) {
reject("reject")
}
})
}
async function asyncFunction() {
try {
return "resolve"
} catch (e) {
throw "reject"
}
}
promiseFunction().then(txt => {
console.log(txt);
}).catch(e => {
console.error(e);
})
async function main() {
const txt = await asyncFunction()
console.log(txt);
}
main()
少し比較しづらくなりましたが、awaitはthen()とcatch()を置き換えている、ということがわかりますね。
非同期関数に対してawaitを使うことでresolve()やreject()で返される値を待つことができます。
async / await の動作確認でよくやるミス
async / awaitを理解しようとサンプルコードを書いているときに、よくある(?)躓きポイントです。ここで引っかかると非同期処理がよくわからなくなると思ったので載せておきます。
まずは該当のサンプルコードをご覧ください。
async function asyncFunction() {
try {
setTimeout(()=>{
return "resolve"
}, 1000)
} catch (e) {
throw "reject"
}
}
async function main() {
const txt = await asyncFunction()
console.log(txt);
}
main()
async / awaitはPromiseの糖衣構文なのだから、これでOKでしょ!と思って実行すると
console.logの結果がundefindとなってしまいました。
awaitは非同期処理に対して、resolveかrejectが返されるまで待ってくれるはずなのに、このサンプルコードでは待ってくれませんでした。
一体どうしてでしょうか。
これは、JavaScriptのコールバック関数に関する話が絡んできます。
setTimeoutは引数にコールバック関数をとり、その中身を第2引数で指定したミリ秒後に実行します。
setTimeout(コールバック関数, 遅らせるミリ秒)
つまり、上記コードでsetTimeoutのコールバック関数はasync functionではなく、通常の関数になっているため、returnはresolveではなく、通常の関数のreturnになってしまっているのです。
じゃあ、setTimeout(async () => {})とすればいいじゃないか、と思う方もいらっしゃるかもしれません。
async function asyncFunction() {
try {
setTimeout(async ()=>{
return "resolve"
}, 1000)
} catch (e) {
throw "reject"
}
}
async function main() {
const txt = await asyncFunction()
console.log(txt);
}
main()
しかし、これもまたNGなコード例です。
何故かというと、setTimeoutのコールバック関数を非同期関数にし、その中でreturnすなわちresolveをしても、その戻り値の送り先はsetTimeoutを呼び出している関数自身になります。(今回の場合はasyncFunctionが該当します)
そのため、asyncFunctionはsetTimeoutからの戻り値を何かしらの形(変数など)で受け取っていないので、結果としてresolveは消失してしまっているのです。
それならば、return setTimeoutにすればいいじゃないか!
こういうことですね。
async function asyncFunction() {
try {
return setTimeout(async ()=>{
return "resolve"
}, 1000)
} catch (e) {
throw "reject"
}
}
async function main() {
const txt = await asyncFunction()
console.log(txt);
}
main()
残念ながらこれもまたNGな例です。
setTimeoutメソッドは戻り値としてtimerIdを返します。このtimerIdはclearTimeoutメソッドへ渡すことで、タイムアウトを取り消すことができるものです。
参考:MDN - SetTimeout
そのため、setTimeoutのコールバック関数内でいくらreturnをしたところで、そのreturnはどこへ渡るでもなく消えてしまうのです。
つまり、async / awaitの検証で処理を遅らせるために、setTimeout()を用いることはできない、ということです。
一方でPromiseではresolveで戻り値を返すので、関数のreturnと認識されずに済む、という訳です。
以上、JavaScriptのややこしい小話でした。
then, catch, await
ここで、then()とcatch()はPromiseに対してのみ使えて、awaitはasyncに対してのみ使えるのか?と疑問に思う方が居るかもしれません。
async / awaitはPromiseの糖衣構文という事からも想像ができますが、お互いに利用可能です。
こちらもサンプルコードで例をみていきましょう。
function promiseFunction() {
return new Promise((resolve, reject) => {
try {
resolve("resolve")
} catch (e) {
reject("reject")
}
})
}
async function asyncFunction() {
try {
return "resolve"
} catch (e) {
throw "reject"
}
}
// promiseFunction() から asyncFunction()に入れ替え
asyncFunction().then(txt => {
console.log(txt);
}).catch(e => {
console.error(e);
})
async function main() {
// asyncFunction() から promiseFunction()に入れ替え
const txt = await promiseFunction()
console.log(txt);
}
main()
このように、先ほどのサンプルコードを入れ替えたものでも無事に動くことが確認できます。
以上がasync / awaitがPromiseの糖衣構文というお話でした。
極論JavaScriptで非同期処理を扱いたい場合は、Promiseかasync / awaitのどちらかが使えれば問題ないということです。
ここまで覚えていればJavaScriptにおける非同期処理は問題なく利用することができますが、さらに余力のある方は次に説明する非同期処理を並列で行うPromise.allについても理解しておくと、よりスッキリとしたコードが書けると思います。
非同期処理を並列で行う Promise.all について
Promise.all()メソッドはPromiseオブジェクトの配列を受け取り、全てのPromiseオブジェクトがresolveされたタイミングでthenが呼び出されます。
Promise.all の基本構文
Promise.allは、上記でも説明した通り、配列内の全てのオブジェクトがresolveされたタイミングでthenメソッドが呼ばれます。
Promise.all([taskA, taskB]).then(() => {})
こちらもサンプルコードをみてみましょう。
function promiseFunction() {
return new Promise((resolve, reject) => {
try {
setTimeout(() => {
resolve("resolve")
}, 1000)
} catch (e) {
reject("reject")
}
})
}
async function asyncFunction() {
try {
return "resolve"
} catch (e) {
throw "reject"
}
}
const asyncTaskA = promiseFunction()
const asyncTaskB = asyncFunction()
Promise.all([asyncTaskA, asyncTaskB]).then(() => {
console.log(`全てのタスク完了!`);
})
このように、全ての非同期処理が完了するまで待機することができるので、いくつかのDBから値をとってきて、両方の値を利用して処理Aを行う、といったシチュエーションでとても役立つメソッドです。
ここで注意すべき点として、いずれかの非同期処理が1つでもrejectしてしまうと、Promise.allのthenが呼び出されない、という点です。
こちらもサンプルコードをみていきましょう。
function promiseFunction() {
return new Promise((resolve, reject) => {
try {
setTimeout(() => {
throw "意図的なエラー"
resolve("resolve")
}, 1000)
} catch (e) {
reject("reject")
}
})
}
async function asyncFunction() {
try {
return "resolve"
} catch (e) {
throw "reject"
}
}
const asyncTaskA = promiseFunction()
const asyncTaskB = asyncFunction()
Promise.all([asyncTaskA, asyncTaskB]).then(() => {
console.log(`全てのタスク完了!`);
})
このように、いずれかの非同期処理が失敗すると、Promise.allのthenが呼ばれずに、"全てのタスク完了!"というログが出力されていないことがわかります。
これはこれでありがたい仕様ですが、たとえ1つがrejectされても続行してほしい!というシチュエーションもあるかと思います。
そんな時に役立つのがES2020で導入されたPromise.allSettled()です。
Promise.allSettled
Promise.allSettledはPromise.allと比べて、いずれかのタスクがrejectを起こしても、その後のthenが呼ばれるメソッドです。
こちらもサンプルコードをみていきましょう。
function promiseFunction() {
return new Promise((resolve, reject) => {
reject("reject")
})
}
async function asyncFunction() {
try {
return "resolve"
} catch (e) {
throw "reject"
}
}
const asyncTaskA = promiseFunction()
const asyncTaskB = asyncFunction()
Promise.allSettled([asyncTaskA, asyncTaskB]).then(() => {
console.log(`全てのタスク完了!`);
})
このように、promiseFunction()がrejectを返したとしても、"全てのタスク完了!"というログが出力されていることがわかります。
Promise.race
Promise.raceは2つ以上ののPromiseのうちの1つがresolveまたはrejectするとすぐに、そのPromiseの値または理由で解決または拒否する Promise を返します。
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, 'one');
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'two');
});
Promise.race([promise1, promise2]).then((value) => {
console.log(value);
});

このように、Promiseの中で最初に解決されたpromise2で返されている値がログが出力されていることがわかります。
Promise.any
続いてES2021で追加されたPromise.anyです。
Promise.receでは最初にresolveもしくはrejectされた値を返しますが、Promise.anyでは最初にresolveされた値を返します。
const pErr = new Promise((resolve, reject) => {
reject("Always fails");
});
const pSlow = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "Done eventually");
});
const pFast = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "Done quick");
});
Promise.any([pErr, pSlow, pFast]).then((value) => {
console.log(value);
})

このように、最初にreject("Always fails");が完了しますが、Promise.anyでは最初にresolveされた値を返すので、Done quickが返されています。
以上、JavaScriptの非同期処理についての解説でした。
Discussion
ぶ厚いJavaScript参考書を読んだり、有料のWEB記事を読んでも非同期通信・Promiseについて全然理解できなかった人間です。
ですが、この記事はとてもわかりやすくて理解できました。
ありがとうございます。
嬉しいお言葉ありがとうございます!
お役に立てたようで何よりです😆