async/awaitとは何か?使い方や注意点を実例で解説(try, catch, finally,逐次実行と並列実行の違い)

javascript-prograshi(プロぐらし)-kv JavaScript
記事内に広告が含まれていることがあります。

JavaScriptを使っているとasync・awaitを使ったコードを見る事があります。

これは、非同期処理をまるで同期処理のように、驚くほどシンプルで読みやすいコードで記述できる便利な構文です。

本記事では、このasyucやawaitがそれぞれ何をしているのか?具体的にどのように使うのか、try…catch…finally を使ったエラー処理方法、さらにパフォーマンスを最大限に引き出すための逐次実行と並行実行(Promise.all)の使い分けを、具体的な実例を用いて解説します。


asyncとawaitとは何か?

asyncとawaitは、Promiseの非同期処理を、より直感的な同期処理に近い書き方で記述できるようにした構文です。

簡単に言うと、Promiseをより簡単に記述できるようにしたものです。


なお、asyncは関数の前につけるキーワードで、awaitはその関数の中で同期処理を行うために使うキーワードです。

asyncとawaitを理解する上では「非同期処理」「同期処理」「Promise」の理解が欠かせないので、まずはその3つについて簡単に解説します。


非同期処理と同期処理

非同期処理とは何か?

非同期処理とは、時間のかかる処理を待たずに、同時進行で次の処理を進める仕組みのことです。時間のかかる処理をバックグラウンドに任せ、その間に他の処理を進めることができます。(ノンブロッキングといいます)

この仕組みによって、アプリやウェブサイトがフリーズせずに動き続けることができます。

開始は順番通りでも、完了は順不同になることがあります。

MEMO

非同期処理は英語で「Asynchronous Processing」です。冒頭の5文字をとって「Async」と表記されることもあります。


同期処理とは何か?

通常のコードは上から順番に一つ一つ処理を実行していきます。この処理を「同期処理」と言います。

MEMO

同期処理は英語で「Synchronous Processing」です。冒頭の4文字をとって「Sync」と表記されることもあります。


Promiseと何か?

Promise(プロミス)は JavaScript における非同期処理を扱うためのオブジェクトです。

JavaScriptにおけるPromiseは「将来の処理の完了を約束する」という意味で、必ず処理結果として「成功」または「失敗」を返します。

時間がかかる処理(非同期処理)をするときに使う、JavaScriptの便利な仕組みです。


asyncとawait

asyncとは何か?

async(アシンク)とは、非同期という意味の「Asynchronous」の略です。

JavaScriptではasyncを関数の前に付けるキーワードとして使います。asyncをつけるとで、その関数が非同期関数である宣言となります。

asyncを付けた関数は、必ずPromiseを返します

つまり、Promiseを簡単に書きたければ、関数を定義した際に、一番前に「async」をつければOKということです。

async function 関数名() { ... }

こうすることで、指定した関数は自動的にPromiseになります。

つまり、以下のコードを書いたことと同じになります。

function 関数名() {
  return new Promise((resolve, reject) => { ... });
}


awaitとは何か?

await(アウェイト)は、英語で「待つ」という意味の動詞(他動詞)です。

JavaScriptでは、async関数の中でのみ使用できるキーワードで、かつ、その後にPromiseが来る場合にのみ機能します。

awaitの後にPromiseを置くことで、そのPromiseが結果(成功または失敗)を返すまで、次の行のコードの実行を一時停止します。つまり、部分的に非同期ではなく同期処理になります

結果が出たら、その値を受け取って、次の行のコードを再開します。

// async をつけるだけで、この関数は Promise を返す関数になる
async function 関数A() {
    const data = await fetch('api/data'); 
    return data.json();
}



asyncをつけた関数のデータの受取り方法

成功時のデータの受取りはreturnとthen

Promiseの場合、非同期処理の結果を受け取るにはresolveとthenを使います。(new Promise((resolve, reject) => { … })の場合)

asyncをつけた関数のデータを受け取るにはreturnthenを使います

async function getHello() {
    return "こんにちは";
}

getHello().then(console.log);


getHello関数を実行するとその結果はPromiseオブジェクトになります。上記コードの場合は処理は成功するので、結果をthenの引数に渡します。

thenで受け取った結果をconsole.log関数の引数に渡します。

補足

.then(console.log)は見慣れない書き方かもしれません。これは以下の処理と同じです。

async function getHello() {
    return "こんにちは";
}

getHello().then(data => console.log(data));


asyncをつけた関数の結果はPromiseオブジェクト

asyncをつけた関数を実行した結果、通常の関数のようにreturnで指定した値をそのまま取得できるわけではありません。

結果はPromiseオブジェクトになっています。

async function getHello() {
    return "こんにちは";
}

console.log(getHello());


これは以下のように、asyncをつけた関数内のreturnはPromiseで包まれるためです。

Promise.resolve("こんにちは")


エラーはcatchで受け取る

asyncで発生したエラーを受け取るにはcatchを使います。

// Promiseが失敗してエラーを返す関数
async function fetchDataWithError() {
    // throwは失敗を返す
    throw new Error("ネットワーク接続が切れました"); 
}

fetchDataWithError().catch(error=>console.log(error.message));
補足

catchで取得したデータはエラーオブジェクトになっています。その中のメッセージを取得したい場合は、messageプロパティを指定する必要があります。

async function getHello() {
    return "こんにちは";
}

getHello().then(data => console.log(data));



try/catch構文の利用

try/catchでエラーを捕まえる

実際にasyncを使う場合は、エラーを捕まえる方法として、asyncを指定した関数の中でtry/catch構文を使うことが一般的です。

async function example() {
  try {
    const a = await mightFailA();
    const b = await mightFailB(a);
    console.log("成功:", a, b);
  } catch (err) {
    console.error("どこかで失敗:", err);
  }
}


こうすることで、わざわざ、asyncで実行した関数を呼び出すときにエラーのハンドリング処理を記述する必要がなくなります。


なお、エラーが発生した箇所をより具体的に特定した場合は、asyncをつけた関数の中でtry/catchを分割します。

async function example() {
  try {
    const a = await mightFailA();
  } catch (err) {
    console.error("Aで失敗:", err);
    return; // 以降を実行しない
  }

  try {
    const b = await mightFailB();
  } catch (err) {
    console.error("Bで失敗:", err);
  }
}


try/catch/finally

tryの中で、処理が成功しようが失敗しようが必ず実行したい処理にはfinallyを使います。

async function example() {
  try {
    const a = await mightFailA();
    const b = await mightFailB(a);
    console.log("成功:", a, b);
  } catch (err) {
    console.error("どこかで失敗:", err);
  } finally {
    nesessaryFunc();
  }
}


throwの利用(Promiseの失敗を返す)

throwを使うと、強制的にエラーを発生(例外処理)させ、強制的に中断させることができます。

特にthrowをasyncの中で使うと、throwされたエラーは、失敗のPromiseを返します。

async function checkInventory(status) {
    if ( status = "在庫なし" ) {
        throw new Error("在庫不足です"); 
    }
    return "商品準備OK";
}

async function processOrder() {
    let status = "在庫なし"
    try {
        await checkInventory( status );
    } catch (error) {
        console.log(`エラー発生: ${error.message}`); 
    }
}

processOrder();


throwを使うときは、エラーオブジェクトを作るのが一般的です。例えば以下のような使い方があります。

throw new Error("エラーメッセージ"); 
throw new ValidationError("名前は必須です");
throw new UserException("無効なデータが入力されました");



awaitの必要性(awaitの有無で処理がどう変わるか)

asyncをつけた関数の中で、awaitをPromiseの前につけると同期処理にすることができます。

これが具体的にどういうことかを実際のコードで解説します。


awaitがない場合

以下のように2秒かかるPromiseの処理「delayTask関数」があるとします。

これをawaitなしで呼び出します。

// 2秒かかる処理
function delayTask() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("★2秒後に完了しました");
    }, 2000);
  });
}


//awaitなしの場合
async function withoutAwait() {
  console.log("01.処理開始");

  const result = delayTask(); //awaitなしでPromiseの処理を実行
  
  console.log(result); // ← Promise のまま
  console.log("02.処理終了");
}

withoutAwait();


すると、delayTask()の処理の完了を待たないので、console.log(result)はPromiseオブジェクトのままになります。


awaitがある場合

awaitを使って、delayTask()の処理の完了を待つと結果が変わります。

// 2秒かかる処理
function delayTask() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve("★2秒後に完了しました");
    }, 2000);
  });
}


//awaitありの場合
async function withoutAwait() {
  console.log("01.処理開始");

  const result =  await delayTask(); //awaitなしでPromiseの処理を実行
  
  console.log(result); // Promiseが成功した結果を返す
  console.log("02.処理終了");
}

withoutAwait();


awaitを使うことで、結果が出るまで待ち、中身を取り出して使うことができます。



awaitの処理を効率的に記述する!(並列実行と逐次実行)

awaitは非同期処理の中で順序正しく同期処理を行える便利なキーワードですが、多用すると多くの処理が順番待ちとなり非効率になります。これを逐次実行と言います。

より効率的に処理を行う方法に並列実行があります。


逐次実行とは?

awaitを使って一つ一つのPromiseの処理が完了するのを待つことを逐次実行と言います。

以下の例だと、欲しい結果はresult1, result2, result3の3つですが、この結果をそれぞれ取得するために、asyncFuncA, B, Cの順に順番に処理を実行します。

async function slowFunc() {
  const result1 = await asyncFuncA(); // Aが終わるまで待機
  const result2 = await asyncFuncB(); // Aが終わってから、Bが終わるまで待機
  const result3 = await asyncFuncC(); // Bが終わってから、Cが終わるまで待機
  return [result1, result2, result3];
}


並列実行とは?

並列実行は複数のPromiseの作業を並列で処理し、最後にそれぞれの結果が出そろうまでawaitで待つ方法です。

例えば以下であれば、asyncFuncA, B, Cがすぐに実行されます。

その後の、awaitでA, B, C全ての結果の終了を待って、から次のreturnへ進むことができます。

async function fastFunc() {
  const promiseA = asyncFuncA();
  const promiseB = asyncFuncB();
  const promiseC = asyncFuncC();

  const result1 = await promiseA;
  const result2 = await promiseB;
  const result3 = await promiseC;

  return [result1, result2, result3];
}



上記コードをPromise.allを使った処理に書き換えることもできます。

async function fastFunc() {
  const promiseA = asyncFuncA();
  const promiseB = asyncFuncB();
  const promiseC = asyncFuncC();

  const [result1, result2, result3] = await Promise.all([promiseA, promiseB, PromiseC]); // 完了を待機
  return [result1, result2, result3];
}

タイトルとURLをコピーしました