Dartにおける非同期プログラミングとエラーハンドリング

More than 3 years have passed since last update.

この記事はバージョン1.6時点の物です


Dartにおける非同期プログラミング


Dartはシングルスレッド

DartはWebアプリ開発のために設計された言語なのでシングルスレッドで動作します。Dartの非同期プログラミングはシングルスレッドでのコールバックとハンドリングによって実現されます。


コールバック地獄

一般にコールバックは次のような入れ子地獄を容易に引き起こします。

asyncA((a) {

asyncAToB(a, (b) {
asyncBToC(b, (c) {
asyncSetC(c, () {
print("done");
});
});
});
});

これはDartのように関数がオブジェクトである言語であればハンドラーに名前をつけることによって幾分か可読性は上がります。

handleSetC() => print("done"); 

handleBToC(c) => asyncDone(c, handleSetC);
handleAToB(b) => asyncBToC(b, handleBToC);
handleA(a) => asyncAToB(a, handleAToB);
asyncA(handleA);

しかし入れ子が無くなっても処理の流れは読みにくく、簡単とはいえません。


Futureによる非同期プログラミング

dart:asyncパッケージが提供するFutureは上記のような非同期プログラミングを簡潔に記述できるようにしてくれます。例えば上のコールバックはasyncAFutureを返すようにすると次のように書き換えられます。

asyncA() // returns a Future

.then(asyncAToB)
.then(asyncBToC)
.then(asyncSetC)
.then((_) => print("done");

FutureはEcmaScriptでいうPromiseであり、コールバック・ハンドリングによる非同期プログラミングを簡単にしてくれます。


エラーハンドリングとZone

Futureの基本はthencatchErrorです。非同期処理が完了した時、値かエラーのどちらかが必ず返されます。値が返ってきた場合はthen、エラーが返ってきた場合はcatchErrorがコールバックされます。thenの内部でエラーが投げられた場合もcatchErrorにジャンプします。

void main() {

var future = new Future(() => 1);
future.then((_) => throw "error")
.catchError((e) => print(e));
}


stdout

error


しかしFutureが入れ子になっていたらどうなるでしょうか?

void main() {

var future = new Future(() => 1);

future.then((_) {
var innerFuture = new Future(() {
throw "innerError";
});
throw "error";
})
.catchError((e) => print(e));
}


stdout

error

Unhandled exception:
Uncaught Error: innerError
Stack Trace:
...

これを実行すると、"innerError"printされず、ハンドリングされていないことがわかります。Futureの非同期処理はそれぞれ固有の Zone という範囲内で行われます。catchErrorはそのFutureが属しているZoneの内部で発生したエラーしかハンドルすることができません。ですので、"innerError"をハンドルするには次のように書く必要があります。

void main() {

var future = new Future(() => 1);

future.then((_) {
var innerFuture = new Future(() {
throw "innerError";
}).catchError((e) => print(e));
throw "error";
})
.catchError((e) => print(e));
}

しかし、runZonedを使うと、入れ子になったFutureも全て同じZoneで実行させることができます。runZonedの第一引数に渡した関数内で発生したエラーは全てonError引数でハンドルされます。

void main() {

var future = new Future(() => 1);

runZoned(() {
future.then((_) {
var innerFuture = new Future(() {
throw "innerError";
});
throw "error";
});
}, onError: (e) => print(e));
}


stdout

error

innerError

このようにしてDartでは非同期プログラミングでもtry-catchのようにエラーハンドリングすることができます。