なぜ「例外」は現代のDartにおいて「悪」なのか?
多くのエンジニアが犯す過ちは、`try-catch` を「予期せぬエラーを防ぐための魔法の杖」だと勘違いしていることだ。
Dartの `Exception` や `Error` は、呼び出しスタックを巻き戻し、意図しない制御フローの分岐を引き起こす。特に非同期処理が絡む Flutter アプリケーションにおいて、深い階層で発生した例外を適切に補足し損ねれば、ユーザーの目の前でアプリはクラッシュする。
我々が目指すべきは、「例外を発生させない」ことではなく、「失敗を型として定義し、呼び出し元に強制的に処理させる」ことだ。 Dart 3で導入されたパターンマッチングと代数的データ型(ADT)の概念を使えば、これを極めてエレガントに実現できる。
—
堅牢な Result 型の設計
まずは、この設計の根幹となる `Result` 型を定義しよう。ここで重要なのは、`sealed class` を使うことだ。これにより、コンパイラは `switch` 式において「網羅性(Exhaustiveness)」を保証する。
/// 成功と失敗を表現する Result 型
/// sealed class を使うことで、switch での漏れをコンパイル時に検知できる
sealed class Result {
const Result();
}
class Success extends Result {
final S value;
const Success(this.value);
}
class Failure extends Result {
final F error;
const Failure(this.error);
}
なぜこれが「最強」なのか?
もしあなたが `switch` で `Success` しか処理せず `Failure` を書き忘れた場合、Dartコンパイラは即座にエラーを吐く。これにより、「エラーハンドリングを忘れる」というバグを物理的に排除できる。
—
実践:API連携における「型安全なフロー」
現場でよくある、APIコールを抽象化したサービス層の例を見てみよう。
// 失敗の定義も enum や sealed class で構造化するのがプロの作法
sealed class NetworkError {
const NetworkError();
}
class ServerError extends NetworkError {}
class TimeoutError extends NetworkError {}
class ApiClient {
Future
try {
// 本来は Dio などのライブラリを使用
final response = await Future.delayed(
const Duration(seconds: 1),
() => “Payload Data”
);
return Success(response);
} catch (_) {
// 例外をキャッチして「型」に変換する
return Failure(ServerError());
}
}
}
—
パターンマッチングによる「宣言的」な制御フロー
呼び出し側(UIコンポーネントやRepository層)では、もはや `try-catch` を書く必要はない。`switch` 式を用いて、戻り値の型に合わせて処理を分岐させる。
void handleApiResult() async {
final client = ApiClient();
final result = await client.fetchData();
// switch 式で完全に制御する
final message = switch (result) {
Success(value: final data) => ‘Success: $data’,
Failure(error: ServerError _) => ‘Error: サーバーが死んでる’,
Failure(error: TimeoutError _) => ‘Error: タイムアウト’,
};
print(message);
}
パフォーマンスへの配慮
「パターンマッチングは遅いのではないか?」という懸念を持つかもしれない。しかし、Dart VM における `switch` 式は、特に `sealed class` を対象とした場合、型判別(Type Test)の最適化が極めて強力だ。従来の `if-else` や `instanceof` の羅列に比べ、VMは型インデックスに基づくジャンプテーブルに近い最適化を行うため、計算コストは最小限に抑えられる。
—
テクニカルリードからの提言
この設計を導入する際に、必ず守るべきルールが一つある。
「ドメインの境界を超えて例外を伝播させないこと」
Repository 層から UI 層まで、すべての関数のシグネチャを `Future
- 保守性の向上: 関数定義を見るだけで、どのような失敗パターンが想定されているか、開発者は即座に理解できる。
- 認知負荷の低減: `try-catch` のネスト地獄から解放され、コードがフラットで読みやすくなる。
Dart 3のパターンマッチングは、単なるシンタックスシュガーではない。それは、「予測不能なランタイムエラー」を「コンパイル時に制御可能なデータ」へと昇華させるための強力な武器だ。
今日からあなたのプロジェクトで、例外を「投げる」のをやめ、Result を「返す」設計に変えてほしい。それが、世界最高峰のアーキテクチャへの第一歩だ。