APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

async/await を使って連続する非同期処理の整合性を保つ

はじめに

先進サービス開発事業部の高橋です。 JavaScriptの非同期処理の終了タイミングを計るという行為は少し癖があって扱いにくい印象がありましたが、ES2017から追加されたasync/awaitを使うとかなり楽に実装できるようになりました。 そこで、複数の連続する非同期処理を一纏まりの処理として扱い、途中で失敗したときには差し戻しを行うと行った、RDBでいうところのトランザクションのような処理をJavaScriptで実装する方法について記載します。

Promise

まず、非同期で処理を行う関数でPeomiseのインスタンスを返します。Peomiseには非同期処理の終了、もしくは失敗の結果が入ります。

インスタンスの初期化をする際にコンストラクタの引数にexecutor関数を定義することができます。executor関数では成功した時の値をセットするresolve関数と、失敗した際の値をセットするreject関数を引数に取ることができます。

const hidouki = () => {
  return new Promise((resolve, reject) => {
    // 何かしらの非同期処理
    // 成功時には resolve();
    // エラー時には reject();
  });
}

async / await

次に、Promiseを返す関数を受け取る側の関数を定義します。 この関数でasync/awaitを用いて非同期処理の終了タイミングを取得しつつ、try...catchで例外(エラー)のハンドリングを行います。

const hoge = async () => {
  try {
    await hidouki1();
    await hidouki2();
    await hidouki3();
  } catch(e) {
    // エラーがスローされた場合
    // TODO: エラー時の処理を記述
  }
};

Promiseを返す非同期処理でreject関数がコールされた場合には、例外(エラー)がスローされた形となり、以降の処理を中断しcatchブロックのスコープに入ります。

サンプルコード

簡単なサンプルを作成しました。処理内容は以下の通りです

  1. A、B、Cという3種類の非同期処理を実行する
    • Aは1000ミリ秒後に遅延実行される処理
    • Bは500ミリ秒後に遅延実行される処理
    • Cは250秒後に遅延実行される処理
  2. Aが終了したらB、Bが終了したらCという順番を守らせる
  3. いずれかの処理が失敗すると、以降の処理を行わない
  4. 処理の失敗時にはロールバックする

JSFiddleにも動作を確認できるサンプルを置いてあります。

<div>
  <button id='success'>
   全ての処理が成功するパターン
  </button>
</div>
<div>
  <button id='fail'>
   2つ目の処理で失敗失敗するパターン
  </button>
</div>
<p id="result_a"></p>
<p id="result_b"></p>
<p id="result_c"></p>

A、B、Cの処置成功時にそれぞれ #result_a、#result_b、#result_c にテキストを挿入します。 逆に途中で失敗したときには、以降の処理をやめ、挿入されたテキストも削除します。

※ このサンプルでは非同期処理の例としてsetTimeoutを使っており、基本的に失敗は起こりえないため、引数の値を見て擬似的に失敗を引き起こすということをしています。

let success = document.getElementById('success');
let fail = document.getElementById('fail');
let result_a = document.getElementById('result_a');
let result_b = document.getElementById('result_b');
let result_c = document.getElementById('result_c');

success.onclick = () => transaction();
fail.onclick = () => transaction(true, false);

const doa = (su = true) => {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      if (su) {
        result_a.innerHTML = 'A: Done!';
        resolve(true);
      } else {
        reject(true);
      }
    }, 1000);
  });
}

const dob = (su = true) => {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      if (su) {
        result_b.innerHTML = 'B: Done!';
        resolve(true);
      } else {
        reject(true);
      }
    }, 500);
  });
}

const doc = () => {
  return new Promise((resolve) => {
    setTimeout(function() {
      result_c.innerHTML = 'C: Done!';
      resolve(true);
    }, 250);
  });
}

const transaction = async (suA = true, suB = true) => {
  rollback();
  try {
    await doa(suA);
    await dob(suB);
    await doc();
  } catch(e) {
    alert('rollback!');
    rollback();
  }
}

const rollback = () => {
  result_a.innerHTML = '';
  result_b.innerHTML = '';
  result_c.innerHTML = '';
}