第7章 失敗の表現②:Result/戻り値で失敗を返す🧾🙂
この章はね、**「ユーザー入力みたいな“想定内の失敗”を、例外じゃなくて Result で安全に返す」**っていう考え方を、C#でちゃんと手で作って体に入れる回だよ〜🎀✨
1 まず結論 例外と Result のざっくり使い分け⚖️🙂
✅ Result が向いてる失敗
- ユーザー入力ミス(メール形式NG、必須が空、桁数不足など)✍️
- ビジネスルール違反(在庫が足りない、期限切れなど)🧾
- 「起きるのが普通にありえる」失敗(=分岐で扱いたい)🔁
✅ 例外が向いてる失敗
- 通信が落ちた、DBが死んだ、ファイルが壊れてる💥
- そもそもコードの前提が崩壊(null参照、バグ)😵💫
- 「めったに起きない」「回復が難しい」タイプ⚡
.NET の例外ガイドでも、**“まれで本当に例外的なときに例外を使う”**という方向性がはっきり書かれてるよ🧠✨ (Microsoft Learn) 一方で、フレームワーク設計のガイドラインだと「エラーコード返すな」って強めに言ってたりもするのね(※これは“汎用ライブラリ設計”の話が主)📚 (Microsoft Learn)
だからこの教材ではこうするよ👇 **アプリの中では「想定内の失敗=Result」「想定外=例外」**で住み分けしようね🙂🛡️
2 今日作る Result はこれ 失敗を型で表す📦✨

目標🎯
- メソッドが「成功 or 失敗」を 型で必ず返す
- 呼び出し側が catch じゃなくて if / switch で分岐できる
- 失敗理由(コード・表示文・どの項目か)を持てる🧾
3 最小 Result 実装 自作で OK🏗️✨
まずはシンプルで十分!「成功は値あり」「失敗はエラーあり」を守る作りにするよ🛡️
using System;
public sealed record Error(string Code, string Message, string? Field = null);
public readonly record struct Result
{
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public Error? Error { get; }
private Result(bool isSuccess, Error? error)
{
IsSuccess = isSuccess;
Error = error;
}
public static Result Ok() => new(true, null);
public static Result Fail(Error error)
=> new(false, error ?? throw new ArgumentNullException(nameof(error)));
public T Match<T>(Func<T> onSuccess, Func<Error, T> onFailure)
=> IsSuccess ? onSuccess() : onFailure(Error!);
}
public readonly record struct Result<T> where T : notnull
{
public bool IsSuccess { get; }
public bool IsFailure => !IsSuccess;
public T? Value { get; }
public Error? Error { get; }
private Result(bool isSuccess, T? value, Error? error)
{
IsSuccess = isSuccess;
Value = value;
Error = error;
}
public static Result<T> Ok(T value)
=> new(true, value ?? throw new ArgumentNullException(nameof(value)), null);
public static Result<T> Fail(Error error)
=> new(false, default, error ?? throw new ArgumentNullException(nameof(error)));
public TOut Match<TOut>(Func<T, TOut> onSuccess, Func<Error, TOut> onFailure)
=> IsSuccess ? onSuccess(Value!) : onFailure(Error!);
}
ポイントはここだよ👇🙂
Ok(...)/Fail(...)以外で作れない(=変な状態を作りにくい)🔒- 呼び出し側は
Matchで読みやすく分岐できる✨ ErrorにFieldを入れたので UI に返すのが楽🎀
4 例外版あるある から Result 版へリファクタ🧯➡️🧾
題材は「会員登録の入力チェック」ね👩💻✨
4-1 例外でバリデーションしちゃう版⚡
public static Guid Register(string email, string password)
{
if (string.IsNullOrWhiteSpace(email))
throw new ArgumentException("email is empty", nameof(email));
if (!email.Contains("@"))
throw new ArgumentException("email is invalid", nameof(email));
if (password is null || password.Length < 8)
throw new ArgumentException("password too short", nameof(password));
return Guid.NewGuid();
}
これ、呼ぶ側はこうなりがち👇😵💫
- どの例外を catch すべきか迷う
- メッセージがそのままUIに出て事故る
- “入力ミス”が例外ログとして溢れて監視が壊れる💥
4-2 Result で返す版🧾🙂
public static Result<Guid> Register(string email, string password)
{
if (string.IsNullOrWhiteSpace(email))
return Result<Guid>.Fail(new Error(
Code: "email.empty",
Message: "メールが空だよ〜🥺",
Field: "email"));
if (!email.Contains("@"))
return Result<Guid>.Fail(new Error(
Code: "email.invalid",
Message: "メールの形がちょっと変かも!📧",
Field: "email"));
if (password is null || password.Length < 8)
return Result<Guid>.Fail(new Error(
Code: "password.short",
Message: "パスワードは8文字以上にしてね🔐",
Field: "password"));
return Result<Guid>.Ok(Guid.NewGuid());
}
呼ぶ側はこう!スッキリ!✨
var result = Register(email, password);
var message = result.Match(
onSuccess: id => $"登録できたよ〜🎉 id={id}",
onFailure: err => $"失敗😢 {err.Message}(field={err.Field}, code={err.Code})");
Console.WriteLine(message);
5 Result で「失敗理由」を育てるコツ🌱🧾
✅ Error の持ち方おすすめ
Code:機械向け(ログ・翻訳・分岐)🤖Message:人向け(表示文)🙂Field:UIで該当欄を赤くする用🎯
将来「APIで返す」になっても、Code があるとめちゃ強いよ💪✨
(.NET 10 の ASP.NET Core も OpenAPI/最小API強化が進んでて、エラーを“ちゃんと形にして返す”流れがより自然になってるよ) (Microsoft Learn)
6 ちょい応用 つなげたいときのミニ道具🔗✨
Result が増えてくると「成功なら次へ、失敗ならそこで止めたい」ってなるよね🙂
そのときは Bind(別名 FlatMap)を1個だけ足すと超便利!
public static class ResultExtensions
{
public static Result<TOut> Bind<TIn, TOut>(
this Result<TIn> result,
Func<TIn, Result<TOut>> next)
where TIn : notnull
where TOut : notnull
{
return result.IsSuccess
? next(result.Value!)
: Result<TOut>.Fail(result.Error!);
}
}
使い方👇
var idResult =
ValidateEmail(email)
.Bind(validEmail => ValidatePassword(password)
.Bind(_ => CreateUser(validEmail)));
※ ここは「便利だけど無理に今全部やらなくてOK」だよ〜🙂🎀
7 実践演習 3ステップで完成🏁🎉
演習A Result を作ってみよう🏗️
- さっきの
Error / Result / Result<T>をプロジェクトに追加 OkとFailを呼ぶだけの小テストコードを書いて動かす ✅ チェック:失敗のときValueを触らない運用になってる?
演習B 例外バリデーションを Result に置換しよう🔁
対象メソッド(何でもOK!)を1つ選んで👇
- 入力ミスを
Result.Fail(Error)に変える - 呼び出し側は
try/catchを消してMatchにする
✅ チェック:例外ログが減って、分岐が読みやすくなった?👀✨
演習C テストで安心を足そう🧪🧸
xUnitでこんなテストを書いてみてね👇
using Xunit;
public class RegisterTests
{
[Fact]
public void Email_empty_should_fail()
{
var r = Program.Register("", "password123");
Assert.True(r.IsFailure);
Assert.Equal("email.empty", r.Error!.Code);
Assert.Equal("email", r.Error!.Field);
}
[Fact]
public void Valid_input_should_succeed()
{
var r = Program.Register("a@b.com", "password123");
Assert.True(r.IsSuccess);
Assert.NotEqual(Guid.Empty, r.Value);
}
}
8 AI活用コーナー ここはAIに任せてOK🤖✨
🪄 便利プロンプト例
- 「このメソッドの入力失敗を Result パターンに置き換えて。Errorは Code/Message/Field を持つ形で」
- 「Result を返す設計にしたい。想定内失敗と想定外例外の線引きをレビューして」
- 「このバリデーションからテストケースを境界値込みで10個作って🧪」
Visual Studio 2026 は AI の統合がより深くなってるので、こういう“定型変換”は特に相性いいよ🤖🛠️ (Microsoft Learn)
9 よくある落とし穴 ここだけ注意してね⚠️🙂
- ✅ Result を返したのにチェックしない(成功前提で
Value!を触る)💥 - ✅ 「全部Result」にしちゃって、通信エラーまで握りつぶす😵💫
- ✅
Messageを技術文のままUIに出す(ユーザーが泣く)🥺 - ✅ 失敗が1種類しかないのに
stringだけで済ませて、後で詰む(Codeが欲しくなる)🧱
10 参考 既存ライブラリを使う選択肢📦✨

自作でもぜんぜんOKなんだけど、現場だと有名どころもあるよ🙂
(教材の演習は自作で進めるのが理解に効くよ🧠💪)
まとめ 今日の持ち帰り🎁🎀
- 想定内の失敗は Result で返すと、呼び出し側が安全に分岐できる🙂🛡️
- Result には Code/Message/Field を持たせると実用度が跳ね上がる📈
- 例外は「想定外」に残しておくと、監視・ログが健康になる🌿✨
次の第8章は、この Result を 境界で“外向け表現”に変換する(画面表示・HTTPレスポンスにする)をやって、さらに実戦っぽくするよ〜🚪🔁🌐