ES7 async/await でのエラーハンドリング

async/await は ES7 の機能で、非同期処理を記述する上で非常に便利な機能である(仕様は安定していないと思う) まだ実装している処理系はないと思うが、babel などの transpiler をつかうと利用できる

async/await をつかうと非同期処理を以下のように書くことができる

function a() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() { resolve('hello, ') }, 0) 
  })
}

async function b() {
  var value = await a()
  return value + 'world'
}

async function c() {
  var value = await b()
  console.log(value)
  return 'this is async world'
}

c().then(function(val) { console.log(val) })

上のコードを babel-node などで実行すると hallo, world に続いて this is async world が表示される。

async function は Promise を返し、 await キーワードにより Promise が解決されるのを待つ(かのように見せる)ことができるので、async/await をつかうと boilerplate なコードを減らして非同期処理を扱うことができる。

TypeScript 実装での async/await の動作

現在の TypeScript の実装から await の動作をも少し詳しく見てみる(仕様は確認していない。。) 上のコードを TypeScript 1.6 で --target ES6 --experimentalAsyncFunctions オプションをつけてコンパイルすると、以下のようなコードが出力される

// runtime
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, Promise, generator) {
    return new Promise(function (resolve, reject) {
        generator = generator.call(thisArg, _arguments);
        function cast(value) { return value instanceof Promise && value.constructor === Promise ? value : new Promise(function (resolve) { resolve(value); }); }
        function onfulfill(value) { try { step("next", value); } catch (e) { reject(e); } }
        function onreject(value) { try { step("throw", value); } catch (e) { reject(e); } }
        function step(verb, value) {
            var result = generator[verb](value);
            result.done
              ? resolve(result.value)
              : cast(result.value).then(onfulfill, onreject);
        }
        step("next", void 0);
    });
};
// 上の async function b はこのように展開される
function b() {
    return __awaiter(this, void 0, Promise, function* () { // --- (2)
        var val = yield a();
        return val + 'world';
    });
}
  • async function は必ず Promise オブジェクトを返す。その Promise オブジェクトは
    • async function が return した場合、その値で resolve する
    • async function の中で throw されると、その値で reject する(例外は catch される)
      • 特別な場合として、一度も await されてない状態で例外が投げられると、その例外は補足されることなくそのまま throw される --- (※)
  • await は何らかの値をとり、その値が Promise でなければ、そのままその値を返す
    • 正確には、Promise でなければ Promise.resolve でラップされる
  • await に渡した値が Promise であれば
    • Promise が resolve した場合 > T (resolve した値) を返す
    • Promise が reject した場合 > reject した値をもって (generator を) throw する

となっている。(余談だが、__awaiter のコードは GitHub - tj/co: The ultimate generator based flow-control goodness for nodejs (supports thunks, promises, etc) とほぼ同様である)

async/await のエラー処理

async/await をつかったときのエラーの流れを見てみる。以下のコードは

function a() { return new Promise(resolve) { setTimeout(function() { resolve('hoge') }, 0) } }
async function b() {
  console.log(await a())
  throw 'hello, '
}
async function c() {
  try {
    await b() // --- (1)
  } catch(e) {  // --- (2)
    throw e + 'error'
  }
}
async function d() {
  var val = await c()
  console.log("control don't reach here")
}
d().catch(function(val) { console.log(val) }) // --- (3)
  1. a で発生したエラーは async function 自体が捕まえ、自身が返す Promise を reject する
  2. (1) の await は Promise が reject したのを受けて、例外を投げるが、すぐに (2) で補足される
  3. エラー内容を更新して再度例外を投げるが、b が async function であるためやはり補足され、b が返す Promise を reject する
  4. c でまた同様に、 catch -> reject -> throw -> catch が行われ、(3) で出力される

となる。 内部的には Promise のエラー処理が走っているのだが、async function より先のコードでは .then .catch はいっさい出てこない。 エラー処理の書き方が同期プログラミングに置ける try ~ catch と同様になるので、Promise のエラー処理に手こずった人でも問題なくなっている

まとめ

ES7 async/await でのエラー処理(throw -> catch -> reject -> throw -> ...)の流れを追った。このフローは try ~ catch が多発してあまりパフォーマンスが欲はなさそうだけど、実際に ES7 として実装されるときはもっと効率的なものになるかもしれない(ので今 TypsScript がエミュレートしているコードを見ても仕方ないかもしれない、そもそも async/await の仕様自体まだ固まってない) ただ、今の時点でこれまでよりずっと直感的な記述が可能になっているし、この後そこまで大きな変更はない?かもしれない。tranpiler で使うのもいいけどはやく普通に使いたいですね。