第14章:エラーの契約②(Result型/戻り値方針)🎁
14.1 この章でできるようになること ✅✨
- 「例外(Exception)にする?」「Result型で返す?」「Tryパターンにする?」を、迷わず選べるようになる🧠💡
- Result型を自作して、公開APIとして“契約”に耐える形で設計できるようになる🏗️✨
- 同じ機能を 例外版 / Result版 の2通りで作り、利用側コードの違いを体験できる👀⚡
- 変更に強い「エラーの表現(Error Code など)」を作れるようになる🧾🔧
14.2 そもそも「エラーの契約」って何?🤔💭
エラーは、成功よりも利用者のコードに刺さりやすいです⚡ なぜなら利用者はこう書くから👇
- 成功時:だいたいそのまま使う😊
- 失敗時:分岐(if/switch)・ログ・再試行・UI表示を組み立てる😵💫
つまり、エラーの返し方がブレると利用者は迷子になります🌀 なので 「失敗をどう表現するか」も契約です📌
14.3 例外 vs Result:世界観の違い🌍✨
例外(Exception)🚨
- 「ここに来たら通常フローじゃないよ!」を強く表現できる💥
- .NETの設計ガイドラインでは、フレームワークのエラー通知は例外が基本(エラーコードを返すのは非推奨)とされています📘(Microsoft Learn)
- ただし、よく起きる失敗を例外で表現するとコストが高くなりがちで、Tryパターン検討が推奨されます🧯(Microsoft Learn)
- さらに「普通に起こる失敗は null/default を返す」という回避策も、ベストプラクティスで言及があります🧩(Microsoft Learn)
Result型(戻り値で成功/失敗を表す)🎁
- 「失敗も通常フローの一部だよ」を表現できる🌿
- 例:入力バリデーション、検索0件、在庫なし、権限不足 など
- 利用側が if で分岐しやすいので、UI/業務ロジックに向いてる😊
14.4 どう選ぶ?判断基準の“最短ルール”⚖️✨

まずこの2択だけ覚える(超重要)💎
- 利用者が普通に分岐して処理する失敗 → Result(またはTry)🎁
- コードの使い方が間違ってる / 想定外 / 続行困難 → 例外🚨
具体例でスパッと分類✂️
- 入力が不正(メール形式NG)✉️ → Result(期待される失敗)🎁
- DBが落ちた / ネットワーク遮断🌩️ → 例外(環境要因で続行困難)🚨
- null渡し禁止なのに null を渡した🙅♀️ → 例外(利用者の使用ミス)🚨
- 「見つからない」検索🔎 → Result(普通に起こる)🎁
- 「TryParseできない」文字列→数値🧮 → Tryパターンが定番(例外回避)🧯(Microsoft Learn)
14.5 Result型を“契約として公開する”ときの注意点📌💣
Result型を public API に出すと、それ自体が契約になります。だから…👇
Result<T>のプロパティ名変更 → 破壊的変更💥Errorの形(Code/Message/Details)変更 → 利用側の分岐が壊れる💥- 「失敗の種類」を enum で表すと、後から値追加で switch が揺れることがある(利用者の書き方次第)🌀
おすすめはこう👇✨
- 失敗理由は 安定した文字列コード(例:
"validation.invalid_email") にする🧾 - 追加情報は
Dictionary<string, string>みたいな拡張枠に入れる🧺 - 例外は「内部ログ用」に保持しても、公開契約には出しすぎない(漏えいリスク)🔒
14.6 実装してみよう:最小で強い Result 型🛠️🎁

ここでは 外部ライブラリなしで、教材用に「ちょうどいい」Resultを作ります✨ (あとで必要なら FluentResults / OneOf / LanguageExt など検討でもOK🙆♀️)
14.6.1 Result と Error を作る(Producer側)🏗️
namespace Contracts;
public readonly record struct Error(
string Code,
string Message,
IReadOnlyDictionary<string, string>? Meta = null);
public readonly record struct Result(bool IsSuccess, Error? Error)
{
public static Result Ok() => new(true, null);
public static Result Fail(Error error) => new(false, error);
}
public readonly record struct Result<T>(bool IsSuccess, T? Value, Error? Error)
{
public static Result<T> Ok(T value) => new(true, value, null);
public static Result<T> Fail(Error error) => new(false, default, error);
}
ポイント🌟
Codeは 機械が分岐するため(安定させる)🧠Messageは 人に見せるため(変更されがちなので依存しすぎない)📝Metaは 拡張枠(後方互換で情報を増やしやすい)➕
14.7 同じ機能を「例外版 / Result版」で作って比べる🔁👀
題材:ユーザー登録のメールチェック✉️
- 失敗はよく起きる(入力ミス)→ Result向き🎁
- でも「例外版」も作って違いを体験するよ😊
14.7.1 例外版(Exception)🚨
using System.Text.RegularExpressions;
namespace Producer;
public static class EmailService_Exception
{
private static readonly Regex EmailLike = new(@"^\S+@\S+\.\S+$", RegexOptions.Compiled);
public static string NormalizeEmail(string email)
{
if (email is null) throw new ArgumentNullException(nameof(email));
var trimmed = email.Trim();
if (trimmed.Length == 0)
throw new ArgumentException("Email is required.", nameof(email));
if (!EmailLike.IsMatch(trimmed))
throw new FormatException("Email format is invalid.");
return trimmed.ToLowerInvariant();
}
}
14.7.2 Result版(Result)🎁
using System.Text.RegularExpressions;
using Contracts;
namespace Producer;
public static class EmailService_Result
{
private static readonly Regex EmailLike = new(@"^\S+@\S+\.\S+$", RegexOptions.Compiled);
public static Result<string> NormalizeEmail(string? email)
{
if (email is null)
{
return Result<string>.Fail(new Error(
Code: "validation.email.null",
Message: "メールアドレスが未入力です🥺"));
}
var trimmed = email.Trim();
if (trimmed.Length == 0)
{
return Result<string>.Fail(new Error(
Code: "validation.email.empty",
Message: "メールアドレスを入力してね✍️"));
}
if (!EmailLike.IsMatch(trimmed))
{
return Result<string>.Fail(new Error(
Code: "validation.email.invalid_format",
Message: "メール形式が正しくないみたい💦",
Meta: new Dictionary<string, string> { ["input"] = trimmed }));
}
return Result<string>.Ok(trimmed.ToLowerInvariant());
}
}
14.8 利用側(Consumer)のコードはどう変わる?🧑💻💡
14.8.1 例外版の利用(try/catch)🧯
using Producer;
try
{
var normalized = EmailService_Exception.NormalizeEmail(inputEmail);
Console.WriteLine($"登録OK: {normalized} 🎉");
}
catch (Exception ex)
{
// 例外の種類で分岐しようとすると、利用側が複雑になりがち😵
Console.WriteLine($"登録NG: {ex.Message} 💦");
}
14.8.2 Result版の利用(if で分岐)🎁
using Producer;
var result = EmailService_Result.NormalizeEmail(inputEmail);
if (result.IsSuccess)
{
Console.WriteLine($"登録OK: {result.Value} 🎉");
}
else
{
// Codeで分岐できるのが強い💪✨
Console.WriteLine($"登録NG: {result.Error?.Message} 💦");
Console.WriteLine($"(code={result.Error?.Code})");
}
Result版の良さ😊
- 利用者が「失敗は普通に起こる」前提で組み立てやすい
- 画面表示・フォームエラー・再入力誘導が作りやすい📱✨
14.9 Tryパターンという“第3の選択肢”🧯✨
.NETの設計ガイドラインでは、例外が頻発しうる操作には Try-Parse パターンが推奨されています✅(Microsoft Learn)
たとえば int.TryParse みたいに👇
public static bool TryNormalizeEmail(string? email, out string normalized)
{
normalized = "";
if (string.IsNullOrWhiteSpace(email)) return false;
var trimmed = email.Trim();
if (!trimmed.Contains('@')) return false; // 簡易チェック例
normalized = trimmed.ToLowerInvariant();
return true;
}
ただし注意💡
- 失敗理由を返しづらい(false しか返らない)
- なので「理由が大事」なら Result の方が向いてることが多い🎁
14.10 “契約として強い”エラーコード設計🧾🔩
14.10.1 良い Code の条件 ✅
- 安定している(文章の変更で揺れない)🧊
- 階層化されている(検索しやすい)🧭
- 利用側が switch/if で分岐できる🧠
例:おすすめ命名🍡
validation.email.emptyvalidation.email.invalid_formatdomain.user.already_existsauth.forbidden
14.10.2 Message は“UI表示用のヒント”にする📝
- Message にロジックを依存させない🙅♀️
- 分岐は Code、表示は Message、追加情報は Meta 🧺✨
14.11 テストで「エラー契約」を固定する🧪🔒
Result型は、テストで契約をガチガチに守るのが強いです💪✨
using Producer;
using Xunit;
public class EmailServiceResultTests
{
[Fact]
public void Null_returns_error_code()
{
var r = EmailService_Result.NormalizeEmail(null);
Assert.False(r.IsSuccess);
Assert.Equal("validation.email.null", r.Error?.Code);
}
[Fact]
public void Invalid_format_returns_error_code()
{
var r = EmailService_Result.NormalizeEmail("abc");
Assert.False(r.IsSuccess);
Assert.Equal("validation.email.invalid_format", r.Error?.Code);
}
[Fact]
public void Valid_email_returns_normalized_value()
{
var r = EmailService_Result.NormalizeEmail(" A@B.com ");
Assert.True(r.IsSuccess);
Assert.Equal("a@b.com", r.Value);
}
}
これで「Code が変わったら落ちる」=契約違反が即バレる😎🧪
14.12 AI活用:Copilot/Codex を“契約ブレ防止”に使う🤖🧰
GitHub Copilot や OpenAI 系ツールは、下書き+漏れチェックに使うと強いです✨ (Visual Studio 2026 でも Copilot 連携が前提で進化しています🧠🛠️)(Microsoft Learn)
そのまま使える定番プロンプト集🪄
- 「この public メソッドを Result
版に変換して。Error Code も設計して」 - 「Result
の Error Code 一覧を “契約として安定する” 命名で提案して」 - 「この Result の返し方、破壊的変更になりそうな点をレビューして」
- 「テストを3本追加して。成功と失敗コードを固定したい」
14.13 まとめ🍰✨(この章の持ち帰り)
- 例外は「想定外/利用ミス/続行困難」に強い🚨(ただし多発する失敗には注意)(Microsoft Learn)
- Resultは「よくある失敗を通常フローとして扱う」のに強い🎁
- 公開する Result は Code を中心に契約を固めるのが安全🧾🔒
- テストで Code を固定すると、契約が“自動で守れる”🧪✨
14.14 ミニ実習(提出物)📚✍️
Result<T>とErrorを自作して、Producer 側に置く🏗️- 章内の
NormalizeEmailを 例外版 / Result版 の2つ作る🔁 - Consumerで呼び出し、UI表示(ConsoleでOK)を実装する🖥️✨
- xUnitで Error Code 固定テストを3本書く🧪✅
参考(本章の根拠)📘
- .NET 10 は LTS として提供され、サポートポリシーも公開されています(Microsoft for Developers)
- 例外のベストプラクティス/設計ガイドライン(Tryパターン、エラーコード非推奨など)(Microsoft Learn)
- C# 14 の機能概要(本カリキュラムの前提言語)(Microsoft Learn)