こんにちは!FlutterやDartの開発現場で、日夜コードと向き合っているシニアエンジニアです。
今回は、Dart 3で導入された強力な「switch式」と、関数型言語のパラダイムを取り入れた「Result型によるエラーハンドリングの標準化」についてお話しします。
「例外(Exception)を投げて、`try-catch`でキャッチする」という従来のコードに、少しモヤモヤしたことはありませんか?
「どこでどんなエラーが起きるのか型シグネチャからは見えない」「キャッチし忘れて実行時クラッシュ(予期せぬ例外)が起きた」――そんな現場の課題を鮮やかに解決するのが、今回紹介するアプローチです。
ここをクリアすれば、あなたの書くDartコードの安全性と美しさは一気にプロフェッショナルな領域に到達します。一緒にしっかりマスターしていきましょう!
—
1. なぜ「例外(Exception)」ではなく「Result型」なのか?
まずは、Dartの標準的なエラーハンドリングを思い出してみましょう。
// 従来のよくある書き方(try-catch)
double divide(int a, int b) {
if (b == 0) {
throw Exception(‘ゼロによる除算はできません’); // 例外を投げる
}
return a / b;
}
void main() {
try {
print(divide(10, 0));
} catch (e) {
print(‘エラー捕捉: $e’);
}
}
このコード、一見問題なさそうですが、大規模なアプリになると「この関数はどんな例外を投げるのか」がシグネチャ(関数名や引数)から一切分からないという致命的な弱点があります。呼び出し側はドキュメントを読むか、実装を覗くしかありません。
Result型という「共通の封筒」
そこで登場するのが `Result型` です。
これは、処理が「成功(Success)」したのか「失敗(Failure)」したのかを、データの型として明示的に包んで返す仕組みです。
[ Result型という封筒 ]
├── 成功の場合 ➔ Success(データ)
└── 失敗の場合 ➔ Failure(エラー情報)
例外を「投げる(Throw)」のではなく、「値として返す」ことで、コンパイラの型チェッカーを味方につけることができます。
—
2. Dart 3のクラスとパターンマッチングで「Result型」を実装する
Dart 3では、`sealed class`(網羅性を強制できるクラス)と強力なパターンマッチング(switch式)が使えるため、Result型を自作して運用するのが非常にエレガントになりました。
まずは、ベースとなるResult型を定義してみましょう。
// Result型は Success か Failure のどちらかである(sealed classで状態を限定)
sealed class Result
const Result();
}
// 成功を表すクラス
class Success
const Success(this.value);
final T value;
}
// 失敗を表すクラス
class Failure
const Failure(this.error);
final E error;
}
ここで使われている `sealed class` は、「このファイルを外れると新しい子クラスを作れない」という制限をコンパイラにかけます。これが後ほど、最強の武器になります。
—
3. 実践!switch式でエラーを美しくハンドリングする
では、実際にこのResult型を返す関数を作り、Dart 3のswitch式を使って処理を分岐させてみましょう。
// ユーザーIDからユーザー名を取得する架空のAPI関数
// 例外を投げず、Result
Result
if (userId <= 0) {
return const Failure('無効なユーザーIDです'); // 失敗を値として返す
}
if (userId == 999) {
return const Failure('ユーザーが見つかりませんでした');
}
// 成功時はデータを包んで返す
return const Success('Alice');
}
void main() {
// テストするID
const targetId = 999;
// 関数の結果(Result)を受け取る
final result = fetchUserName(targetId);
// 【ここがDart 3の真骨頂!】switch式による網羅的パターンマッチング
final message = switch (result) {
// 成功パターン:Successの中身を取り出して文字列を作る
Success(value: final name) => ‘ようこそ、$nameさん!’,
// 失敗パターン:Failureのエラーメッセージを取り出す
Failure(error: final msg) => ‘エラーが発生しました: $msg’,
};
print(message);
}
このコードの何が素晴らしいのか?
1. 制御構文ではなく「式(Expression)」である点
`switch (…)` がそのまま値を返すため、`final message = switch(…)` のように変数に直接代入できます。コードが圧倒的に短く、美しくなります。
2. コンパイラによる「網羅性チェック」
もし将来、Result型に「Loading(読み込み中)」という新しい状態を追加したとします。その瞬間、上記の `switch` 式は「すべてのパターンが処理されていません」というコンパイルエラーを吐き出します。
つまり、「新しい状態を追加したのに、処理し忘れてアプリがクラッシュするバグ」をコンパイル段階で100%防げるのです。これこそが、型安全の極みです。
—
4. 陥りやすい文法エラーと注意すべきポイント
初学者の皆さんが、Dart 3のswitch式やResult型を導入する際によくつまずくポイントをいくつかシェアしておきますね。
① `switch文` と `switch式` を混同してしまう
- switch文(従来の書き方):
switch (result) {
case Success(value: v):
print(v);
break; // breakが必要。値を返せない。
}
- switch式(Dart 3の新しい書き方):
final res = switch (result) {
Success(value: v) => v, // 矢印構文(=>)を使い、値を返す。break不要。
Failure(error: e) => e,
};
今回はエラーハンドリングの戻り値を受け取りたいので、必ず「switch式(矢印 `=>` を使う方)」を使います。
② ジェネリクス `` の型推論に頼りすぎる
Dartの型推論は非常に優秀ですが、複雑な関数を組み合わせると、意図せず `Result
—
まとめ
今回は、Dart 3のswitch式とResult型を組み合わせた、モダンで安全なエラーハンドリング手法について解説しました。
- 例外を投げるのをやめ、Result型(Success / Failure)で値を返す
- Dart 3の `sealed class` で状態を限定する
- `switch式` を使って、網羅的かつ直感的に結果を分岐させる
このパターンを身につけると、あなたの書くコードの「堅牢性」が劇的に跳ね上がります。「あ、エラー処理を書き忘れた!」という不安から解放される気持ちよさを、ぜひ実際の開発でも味わってみてください。
ここをクリアできれば、Dartの基本はもうバッチリマスターできていますよ!次のステップへ向けて、自信を持ってコードを書いていきましょう。