Result型
Result型は「成功または失敗」を型で表現する手法。例外を使わずにエラーを明示的に扱い、コンパイラが「エラー処理忘れ」を検出する。Rust, Go, TypeScript での実装を比較する。
82 分で読めます40,961 文字
Result型
Result型は「成功または失敗」を型で表現する手法。例外を使わずにエラーを明示的に扱い、コンパイラが「エラー処理忘れ」を検出する。Rust, Go, TypeScript での実装を比較する。
この章で学ぶこと
- Result型の仕組みと例外との違いを理解する
- 各言語でのResult型の実装を把握する
- Result型のメリットとデメリットを学ぶ
- モナド的なチェーン操作(map, flatMap, andThen)を理解する
- Result型とOption/Maybe型の関係を把握する
- 実務でのResult型の導入パターンを学ぶ
前提知識
このガイドを読む前に、以下の知識があると理解が深まります:
- 基本的なプログラミングの知識
- 関連する基礎概念の理解
- 例外処理 の内容を理解していること
1. 例外 vs Result型
1.1 基本的な違い
例外:
function getUser(id: string): User {
// エラーが発生する可能性が型から見えない
// 呼び出し側は try/catch を忘れるかもしれない
}
Result型:
function getUser(id: string): Result<User, AppError> {
// 型を見るだけで「失敗の可能性がある」と分かる
// コンパイラがエラー処理を強制できる
}
比較:| 例外 | Result型 | |
|---|---|---|
| エラーの可視性 | 型に現れない | 型に現れる |
| 処理の強制 | なし | コンパイラが強制 |
| コード | try/catch | match/map/unwrap |
| パフォーマンス | スタック巻き戻し | 通常の戻り値 |
1.2 なぜResult型が注目されるのか
Result型が注目される理由:
1. 型安全性
→ エラーの種類が型として明示される
→ コンパイラがエラーハンドリングの漏れを検出
→ IDE の補完が効く
2. 明示性
→ 関数のシグネチャを見るだけで失敗の可能性が分かる
→ 隠れた制御フロー(例外の伝播)がない
→ コードレビューが容易
3. パフォーマンス
→ スタックアンワインドが不要
→ スタックトレースの構築が不要
→ 通常の関数リターンと同じコスト
4. 合成可能性(Composability)
→ map, flatMap, andThen でチェーン処理
→ 関数型プログラミングとの親和性
→ パイプライン処理に適している
5. 予測可能性
→ エラーパスが明確
→ テストが書きやすい
→ デバッグが容易
1.3 Result型の数学的背景
Result型の背景にある概念:
直和型(Sum Type / Tagged Union):
Result<T, E> = Ok(T) | Err(E)
→ T か E のどちらか一方を必ず持つ
これは代数的データ型(ADT)の一種:
→ Haskell: Either a b = Left a | Right b
→ Rust: enum Result<T, E> { Ok(T), Err(E) }
→ Scala: Either[L, R] = Left[L] | Right[R]
→ TypeScript: { ok: true; value: T } | { ok: false; error: E }
モナド(Monad)としての Result:
→ flatMap (andThen) で連鎖可能
→ エラーが発生した時点で短絡(Short-circuit)
→ 例外の try/catch と同等の表現力を持つ
2. Rust の Result
2.1 基本的な使い方
// Rust: Result<T, E> は標準ライブラリの型
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<Config, ConfigError> {
let content = fs::read_to_string(path)
.map_err(|e| ConfigError::IoError(e))?; // ? で早期リターン
let config: Config = serde_json::from_str(&content)
.map_err(|e| ConfigError::ParseError(e.to_string()))?;
if config.port == 0 {
return Err(ConfigError::ValidationError("port must be > 0".into()));
}
Ok(config)
}
// エラー型の定義
#[derive(Debug)]
enum ConfigError {
IoError(io::Error),
ParseError(String),
ValidationError(String),
}
// 使い方
fn main() {
match read_config("config.json") {
Ok(config) => println!("Port: {}", config.port),
Err(ConfigError::IoError(e)) => eprintln!("File error: {}", e),
Err(ConfigError::ParseError(e)) => eprintln!("Parse error: {}", e),
Err(ConfigError::ValidationError(e)) => eprintln!("Validation: {}", e),
}
// ? 演算子でチェーン(呼び出し元にエラーを伝播)
// → try/catch の代わりに型でエラーが伝播する
}
// Result のメソッドチェーン
fn process() -> Result<String, Error> {
read_file("input.txt")?
.lines()
.map(|line| parse_line(line))
.collect::<Result<Vec<_>, _>>()?
.iter()
.map(|item| format_item(item))
.collect::<Result<String, _>>()
}2.2 ? 演算子の詳細
// ? 演算子は以下の糖衣構文:
fn read_file(path: &str) -> Result<String, io::Error> {
// これは:
let content = fs::read_to_string(path)?;
// 以下と等価:
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => return Err(e.into()), // From トレイトで変換
};
Ok(content)
}
// From トレイトによるエラー型の自動変換
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self {
AppError::Io(e)
}
}
impl From<serde_json::Error> for AppError {
fn from(e: serde_json::Error) -> Self {
AppError::Parse(e.to_string())
}
}
// From を実装すると ? 演算子で自動変換される
fn load_config(path: &str) -> Result<Config, AppError> {
let content = fs::read_to_string(path)?; // io::Error → AppError
let config: Config = serde_json::from_str(&content)?; // serde::Error → AppError
Ok(config)
}2.3 Result のメソッド一覧
// Result<T, E> の主要メソッド
fn demonstrate_result_methods() {
let ok_val: Result<i32, String> = Ok(42);
let err_val: Result<i32, String> = Err("error".to_string());
// ========== 値の取り出し ==========
// unwrap: Ok なら値を返す、Err ならパニック
let value = ok_val.unwrap(); // 42
// let value = err_val.unwrap(); // パニック!本番コードでは使わない
// unwrap_or: Err の場合のデフォルト値
let value = err_val.unwrap_or(0); // 0
// unwrap_or_else: Err の場合にクロージャで値を生成
let value = err_val.unwrap_or_else(|e| {
eprintln!("Error: {}", e);
0
});
// expect: unwrap と同じだがパニックメッセージを指定
let value = ok_val.expect("Config must be valid");
// ========== 変換 ==========
// map: Ok の値を変換
let mapped: Result<String, String> = ok_val.map(|v| v.to_string());
// map_err: Err の値を変換
let mapped: Result<i32, i32> = err_val.map_err(|e| e.len() as i32);
// and_then (flatMap): Ok の値に関数を適用(チェーン)
let chained: Result<String, String> = ok_val.and_then(|v| {
if v > 0 {
Ok(v.to_string())
} else {
Err("must be positive".to_string())
}
});
// or_else: Err の場合に別の Result を返す
let recovered: Result<i32, String> = err_val.or_else(|e| {
eprintln!("Recovering from: {}", e);
Ok(0)
});
// ========== 判定 ==========
// is_ok / is_err
assert!(ok_val.is_ok());
assert!(err_val.is_err());
// ========== Option との変換 ==========
// ok(): Result<T, E> → Option<T>
let opt: Option<i32> = ok_val.ok(); // Some(42)
let opt: Option<i32> = err_val.ok(); // None
// err(): Result<T, E> → Option<E>
let opt: Option<String> = err_val.err(); // Some("error")
// transpose: Result<Option<T>, E> → Option<Result<T, E>>
let x: Result<Option<i32>, String> = Ok(Some(42));
let y: Option<Result<i32, String>> = x.transpose(); // Some(Ok(42))
}
// collect で Vec<Result<T, E>> → Result<Vec<T>, E>
fn parse_all(inputs: &[&str]) -> Result<Vec<i32>, String> {
inputs
.iter()
.map(|s| s.parse::<i32>().map_err(|e| e.to_string()))
.collect() // 最初の Err で短絡
}2.4 thiserror と anyhow
// thiserror: ライブラリ向けの構造化エラー
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("User not found: {user_id}")]
UserNotFound { user_id: String },
#[error("Email already exists: {email}")]
EmailAlreadyExists { email: String },
#[error("Validation error: {0}")]
Validation(String),
#[error("Database error")]
Database(#[from] sqlx::Error),
#[error("External API error")]
ExternalApi(#[from] reqwest::Error),
#[error("Internal error: {0}")]
Internal(String),
}
// anyhow: アプリケーション向けの柔軟なエラー
use anyhow::{Context, Result, bail, ensure};
fn load_config(path: &str) -> Result<Config> {
let content = fs::read_to_string(path)
.context("Failed to read config file")?; // コンテキスト追加
let config: Config = serde_json::from_str(&content)
.context("Failed to parse config")?;
ensure!(config.port > 0, "Port must be positive, got {}", config.port);
// ensure! は条件が false なら Err を返す
if config.host.is_empty() {
bail!("Host cannot be empty");
// bail! は即座に Err を返す
}
Ok(config)
}
// thiserror vs anyhow の使い分け:
// thiserror: ライブラリ(呼び出し側がエラーの種類を判別する必要がある)
// anyhow: アプリケーション(エラーの詳細は人間向けメッセージで十分)3. Go のエラー
3.1 基本パターン
// Go: 多値返却でエラーを返す
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config: %w", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config: %w", err)
}
if config.Port == 0 {
return nil, errors.New("port must be > 0")
}
return &config, nil
}
// 使い方
func main() {
config, err := readConfig("config.json")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Port: %d\n", config.Port)
}
// Go のエラー処理の特徴:
// → エラーは値(error インターフェース)
// → if err != nil が頻出(賛否あり)
// → errors.Is, errors.As でエラーの判定
// → fmt.Errorf("%w", err) でエラーのラッピング3.2 カスタムエラー型
// カスタムエラー型の定義
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s not found: %s", e.Resource, e.ID)
}
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on %s: %s", e.Field, e.Message)
}
// sentinel error(定数エラー)
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInternalError = errors.New("internal error")
)
// エラーの判定
func handleError(err error) {
// errors.Is: sentinel error との比較
if errors.Is(err, ErrNotFound) {
fmt.Println("Resource not found")
return
}
// errors.As: カスタムエラー型の判定
var validationErr *ValidationError
if errors.As(err, &validationErr) {
fmt.Printf("Validation failed: %s - %s\n",
validationErr.Field, validationErr.Message)
return
}
// 未知のエラー
fmt.Printf("Unknown error: %v\n", err)
}3.3 エラーのラッピングチェーン
// Go 1.13+: エラーのラッピング
func getUser(id string) (*User, error) {
row := db.QueryRow("SELECT * FROM users WHERE id = ?", id)
var user User
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("getUser(%s): %w", id, ErrNotFound)
}
return nil, fmt.Errorf("getUser(%s): database error: %w", id, err)
}
return &user, nil
}
func createOrder(userID string, items []Item) (*Order, error) {
user, err := getUser(userID)
if err != nil {
return nil, fmt.Errorf("createOrder: %w", err)
}
// ...
return &Order{UserID: user.ID}, nil
}
// エラーチェーンの例:
// "createOrder: getUser(user-123): not found"
// errors.Is(err, ErrNotFound) → true(チェーンを辿って判定)
// Go 1.20+: errors.Join で複数エラーの結合
func validateOrder(order *Order) error {
var errs []error
if order.UserID == "" {
errs = append(errs, &ValidationError{Field: "userID", Message: "required"})
}
if len(order.Items) == 0 {
errs = append(errs, &ValidationError{Field: "items", Message: "at least one item required"})
}
if order.Total < 0 {
errs = append(errs, &ValidationError{Field: "total", Message: "must be non-negative"})
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}3.4 Go のエラー処理の議論
Go のエラー処理に対する議論:
賛成派:
→ シンプルで明示的
→ エラーを無視しにくい(lint ツールで検出)
→ スタックトレースのコストがない
→ エラーの伝播が透明
反対派:
→ if err != nil のボイラープレート
→ エラーハンドリングがコードの大部分を占める
→ Result型の map/flatMap のような合成ができない
→ 型によるエラーの網羅性チェックがない
Go 2 の提案(ドラフト):
→ check/handle 構文(2018年提案、未採用)
→ try 組み込み関数(2019年提案、却下)
→ 結局 if err != nil が残り続ける
実務的な対策:
→ ヘルパー関数でボイラープレートを軽減
→ errgroup でゴルーチンのエラー集約
→ 構造化ログでエラーのコンテキストを補強
4. TypeScript での Result型
4.1 シンプルな実装
// TypeScript: Result型の実装
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}
// 使用例
function parseJson<T>(text: string): Result<T, string> {
try {
return ok(JSON.parse(text));
} catch (e) {
return err(`Invalid JSON: ${(e as Error).message}`);
}
}
function validateUser(data: unknown): Result<User, ValidationError> {
if (!data || typeof data !== "object") {
return err({ field: "root", message: "Invalid data" });
}
const { name, email } = data as any;
if (!name) return err({ field: "name", message: "Name is required" });
if (!email?.includes("@")) return err({ field: "email", message: "Invalid email" });
return ok({ name, email } as User);
}
// チェーン的な使い方
function processInput(input: string): Result<User, string> {
const jsonResult = parseJson<unknown>(input);
if (!jsonResult.ok) return err(jsonResult.error);
const userResult = validateUser(jsonResult.value);
if (!userResult.ok) return err(userResult.error.message);
return userResult;
}4.2 高機能な Result クラスの実装
// より高機能な Result 実装
class Result<T, E> {
private constructor(
private readonly _ok: boolean,
private readonly _value?: T,
private readonly _error?: E,
) {}
static ok<T>(value: T): Result<T, never> {
return new Result(true, value);
}
static err<E>(error: E): Result<never, E> {
return new Result(false, undefined, error);
}
// 例外を Result に変換
static fromThrowable<T>(fn: () => T): Result<T, Error> {
try {
return Result.ok(fn());
} catch (e) {
return Result.err(e instanceof Error ? e : new Error(String(e)));
}
}
// async 版
static async fromPromise<T>(promise: Promise<T>): Promise<Result<T, Error>> {
try {
return Result.ok(await promise);
} catch (e) {
return Result.err(e instanceof Error ? e : new Error(String(e)));
}
}
isOk(): this is Result<T, never> {
return this._ok;
}
isErr(): this is Result<never, E> {
return !this._ok;
}
// Ok の値を変換
map<U>(fn: (value: T) => U): Result<U, E> {
if (this._ok) {
return Result.ok(fn(this._value!));
}
return Result.err(this._error!);
}
// Err の値を変換
mapErr<F>(fn: (error: E) => F): Result<T, F> {
if (this._ok) {
return Result.ok(this._value!);
}
return Result.err(fn(this._error!));
}
// flatMap / andThen: Result を返す関数をチェーン
andThen<U>(fn: (value: T) => Result<U, E>): Result<U, E> {
if (this._ok) {
return fn(this._value!);
}
return Result.err(this._error!);
}
// Err の場合にリカバリー
orElse<F>(fn: (error: E) => Result<T, F>): Result<T, F> {
if (this._ok) {
return Result.ok(this._value!);
}
return fn(this._error!);
}
// 値の取り出し
unwrap(): T {
if (this._ok) return this._value!;
throw new Error(`Called unwrap on Err: ${this._error}`);
}
unwrapOr(defaultValue: T): T {
return this._ok ? this._value! : defaultValue;
}
unwrapOrElse(fn: (error: E) => T): T {
return this._ok ? this._value! : fn(this._error!);
}
// パターンマッチ
match<U>(handlers: { ok: (value: T) => U; err: (error: E) => U }): U {
if (this._ok) {
return handlers.ok(this._value!);
}
return handlers.err(this._error!);
}
// Option への変換
toOption(): T | undefined {
return this._ok ? this._value : undefined;
}
}
// 使用例
const result = Result.fromThrowable(() => JSON.parse('{"name": "test"}'))
.map(data => data as { name: string })
.andThen(data => {
if (!data.name) return Result.err(new Error("Name required"));
return Result.ok(data);
})
.mapErr(e => `Validation failed: ${e.message}`);
result.match({
ok: data => console.log(`User: ${data.name}`),
err: msg => console.error(msg),
});4.3 neverthrow ライブラリ
// neverthrow: TypeScript の人気 Result 型ライブラリ
import { ok, err, Result, ResultAsync } from 'neverthrow';
// 基本的な使い方
function divide(a: number, b: number): Result<number, string> {
if (b === 0) return err("Division by zero");
return ok(a / b);
}
// チェーン
function calculateAverage(numbers: number[]): Result<number, string> {
if (numbers.length === 0) return err("Empty array");
const sum = numbers.reduce((a, b) => a + b, 0);
return divide(sum, numbers.length);
}
// map, mapErr, andThen
const result = calculateAverage([10, 20, 30])
.map(avg => avg.toFixed(2))
.mapErr(e => `Calculation error: ${e}`);
// ResultAsync: 非同期版
function fetchUser(id: string): ResultAsync<User, ApiError> {
return ResultAsync.fromPromise(
fetch(`/api/users/${id}`).then(r => r.json()),
(e) => new ApiError("Fetch failed", e as Error)
);
}
function fetchUserOrders(userId: string): ResultAsync<Order[], ApiError> {
return fetchUser(userId)
.andThen(user => {
return ResultAsync.fromPromise(
fetch(`/api/orders?userId=${user.id}`).then(r => r.json()),
(e) => new ApiError("Fetch orders failed", e as Error)
);
});
}
// combine: 複数の Result をまとめる
import { Result as NResult } from 'neverthrow';
function validateForm(data: FormData): Result<ValidatedForm, ValidationError[]> {
const nameResult = validateName(data.name);
const emailResult = validateEmail(data.email);
const ageResult = validateAge(data.age);
return NResult.combine([nameResult, emailResult, ageResult])
.map(([name, email, age]) => ({ name, email, age }));
// 全て Ok なら Ok、1つでも Err なら最初の Err
}
// safeTry: generator ベースの ? 演算子風構文
import { safeTry } from 'neverthrow';
function processOrder(orderId: string): Result<Receipt, AppError> {
return safeTry(function* () {
const order = yield* getOrder(orderId).safeUnwrap();
const user = yield* getUser(order.userId).safeUnwrap();
const payment = yield* chargePayment(user, order.total).safeUnwrap();
return ok({ orderId, paymentId: payment.id, amount: order.total });
});
}4.4 ts-results ライブラリ
// ts-results: もう一つの人気ライブラリ
import { Ok, Err, Result } from 'ts-results';
function parsePort(input: string): Result<number, string> {
const port = parseInt(input, 10);
if (isNaN(port)) return Err(`Invalid port number: ${input}`);
if (port < 1 || port > 65535) return Err(`Port out of range: ${port}`);
return Ok(port);
}
// val プロパティで Ok/Err の値にアクセス
const result = parsePort("8080");
if (result.ok) {
console.log(`Port: ${result.val}`); // 8080
} else {
console.error(`Error: ${result.val}`); // エラーメッセージ
}
// expect: Ok なら値を返す、Err ならメッセージ付きでスロー
const port = parsePort("8080").expect("Port must be valid");
// map と andThen
const configResult = parsePort("8080")
.map(port => ({ port, host: "localhost" }))
.andThen(config => {
if (config.host === "") return Err("Host required");
return Ok(config);
});5. Option/Maybe型との関係
5.1 Option型とは
Option<T> = Some(T) | None
Result<T, E> = Ok(T) | Err(E)
違い:
Option: 値が「あるかないか」
Result: 値が「あるか、なぜないのか」
Option は「エラーの理由がない Result」と言える:
Option<T> ≒ Result<T, ()> // エラーの情報なし
// Rust: Option と Result の相互変換
fn find_user(id: &str) -> Option<User> {
users.get(id).cloned()
}
fn get_user(id: &str) -> Result<User, AppError> {
find_user(id).ok_or_else(|| AppError::UserNotFound {
user_id: id.to_string(),
})
}
// Option のメソッド
let opt: Option<i32> = Some(42);
opt.map(|v| v * 2); // Some(84)
opt.and_then(|v| if v > 0 { Some(v) } else { None });
opt.unwrap_or(0); // 42
opt.ok_or("value is none")?; // Option → Result// TypeScript での Option 型
type Option<T> = T | null | undefined;
// Result と Option の使い分け
function findUser(id: string): Option<User> {
// 「見つからない」は正常なケース → Option
return users.get(id) ?? null;
}
function getUser(id: string): Result<User, NotFoundError> {
// 「見つからない」がエラーのケース → Result
const user = users.get(id);
if (!user) return err(new NotFoundError("User", id));
return ok(user);
}
// 使い分けの指針:
// Option を使う場面:
// → 値が存在しないことが通常の状況
// → 例: Map.get(), Array.find(), cache.get()
//
// Result を使う場面:
// → 失敗の理由を伝える必要がある
// → 例: API呼び出し、バリデーション、ファイル読み込み5.2 Haskell の Either と Maybe
-- Haskell: Either と Maybe は Result と Option の元祖
-- Maybe a = Nothing | Just a
findUser :: String -> Maybe User
findUser userId = lookup userId userMap
-- Either e a = Left e | Right a(Left がエラー、Right が成功)
getUser :: String -> Either AppError User
getUser userId = case findUser userId of
Nothing -> Left (UserNotFound userId)
Just user -> Right user
-- do 記法でチェーン(モナド)
processOrder :: String -> Either AppError Receipt
processOrder orderId = do
order <- getOrder orderId -- Err なら即座に返る
user <- getUser (orderUserId order) -- Err なら即座に返る
payment <- chargePayment user order -- Err なら即座に返る
return Receipt { receiptOrder = order, receiptPayment = payment }
-- これは以下と等価:
processOrder' :: String -> Either AppError Receipt
processOrder' orderId =
getOrder orderId >>= \order ->
getUser (orderUserId order) >>= \user ->
chargePayment user order >>= \payment ->
Right Receipt { receiptOrder = order, receiptPayment = payment }6. Result型 vs 例外の使い分け
6.1 場面別の選択基準
Result型を使うべき場面:
✓ 予期されるエラー(バリデーション、ファイル未存在)
✓ ライブラリ/APIのパブリックインターフェース
✓ 型安全性が重要な場面
✓ エラーの種類が限定的
✓ パフォーマンスクリティカルなコード
✓ 関数型スタイルのコード
✓ エラーの合成が必要な場面
例外を使うべき場面:
✓ 予期しないエラー(プログラミングミス)
✓ 回復不能なエラー(OutOfMemory)
✓ フレームワークが例外を期待する場合
✓ 深いコールスタックからのエラー伝播
✓ コンストラクタやプロパティアクセスでのエラー
✓ 外部ライブラリとの境界
組み合わせ(推奨):
→ ドメインロジック: Result型(予期されるエラー)
→ インフラ層: 例外(ネットワーク、DB障害)
→ 境界(Controller): 例外を Result に変換
6.2 レイヤー別の使い分け
// レイヤー別の使い分け例
// ========== インフラ層: 例外を投げる ==========
class UserRepository {
async findById(id: string): Promise<User | null> {
// DB エラーは例外として伝播
const row = await db.query("SELECT * FROM users WHERE id = $1", [id]);
return row ? mapToUser(row) : null;
}
}
// ========== ドメイン層: Result型を使う ==========
class UserService {
constructor(private repo: UserRepository) {}
async getUser(id: string): Promise<Result<User, UserError>> {
try {
const user = await this.repo.findById(id);
if (!user) return err(new UserNotFoundError(id));
return ok(user);
} catch (error) {
// インフラ例外を Result に変換
return err(new UserServiceError("Database error", { cause: error }));
}
}
async createUser(data: CreateUserDto): Promise<Result<User, UserError>> {
// バリデーション
const validation = validateCreateUser(data);
if (!validation.ok) return validation;
// 重複チェック
const existing = await this.repo.findByEmail(data.email);
if (existing) return err(new EmailAlreadyExistsError(data.email));
try {
const user = await this.repo.create(data);
return ok(user);
} catch (error) {
return err(new UserServiceError("Failed to create user", { cause: error }));
}
}
}
// ========== プレゼンテーション層: Result を HTTP レスポンスに変換 ==========
class UserController {
constructor(private service: UserService) {}
async getUser(req: Request, res: Response): Promise<void> {
const result = await this.service.getUser(req.params.id);
result.match({
ok: user => res.json(user),
err: error => {
if (error instanceof UserNotFoundError) {
res.status(404).json({ error: error.message });
} else {
res.status(500).json({ error: "Internal server error" });
}
}
});
}
}6.3 実務での移行戦略
// 既存の例外ベースのコードから Result型へ段階的に移行する戦略
// Step 1: Result型のユーティリティを用意
function tryCatch<T>(fn: () => T): Result<T, Error> {
try {
return ok(fn());
} catch (e) {
return err(e instanceof Error ? e : new Error(String(e)));
}
}
async function tryCatchAsync<T>(fn: () => Promise<T>): Promise<Result<T, Error>> {
try {
return ok(await fn());
} catch (e) {
return err(e instanceof Error ? e : new Error(String(e)));
}
}
// Step 2: 新しいコードから Result型を使い始める
// 既存コードとの境界で変換
// 例外 → Result 変換
async function getUserSafe(id: string): Promise<Result<User, AppError>> {
return tryCatchAsync(async () => {
// 既存の例外ベースの関数を呼ぶ
return await legacyGetUser(id);
}).then(result =>
result.mapErr(e => new AppError("USER_FETCH_FAILED", e.message))
);
}
// Result → 例外 変換(フレームワークが例外を期待する場合)
function unwrapOrThrow<T, E extends Error>(result: Result<T, E>): T {
if (result.ok) return result.value;
throw result.error;
}
// Step 3: 重要なドメインロジックから順に移行
// バリデーション → ビジネスルール → サービス層 の順で7. 高度なパターン
7.1 Railway Oriented Programming
Railway Oriented Programming(鉄道指向プログラミング):
正常系と異常系を「2本のレール」として表現する。
各関数は Success レールから Error レールに切り替わる可能性がある。
Success ──────→ validate ──→ transform ──→ save ──→ Success
│ │ │
Error ◁──────────┘ ◁─────────┘ ◁──────┘ Error
Result型の andThen(flatMap)はまさにこのパターン:
→ Success の場合のみ次の関数を実行
→ Error の場合はそのまま Error レールを流れる
// Railway Oriented Programming の実装例
type Result<T, E> = { ok: true; value: T } | { ok: false; error: E };
// パイプライン関数
function pipe<T, E>(
initial: Result<T, E>,
...fns: Array<(value: any) => Result<any, E>>
): Result<any, E> {
let result: Result<any, E> = initial;
for (const fn of fns) {
if (!result.ok) return result; // Error レールをそのまま通過
result = fn(result.value);
}
return result;
}
// 使用例: ユーザー登録パイプライン
function registerUser(input: unknown): Result<User, AppError> {
return pipe(
ok(input),
validateInput, // 入力検証
normalizeEmail, // メール正規化
checkDuplicate, // 重複チェック
hashPassword, // パスワードハッシュ化
saveToDatabase, // DB保存
sendWelcomeEmail, // メール送信
);
}
// 各関数は独立してテスト可能
function validateInput(input: unknown): Result<RegisterDto, AppError> {
if (!input || typeof input !== "object") {
return err({ code: "INVALID_INPUT", message: "Invalid input" });
}
// ... バリデーションロジック
return ok(input as RegisterDto);
}
function normalizeEmail(dto: RegisterDto): Result<RegisterDto, AppError> {
return ok({ ...dto, email: dto.email.toLowerCase().trim() });
}7.2 Result の並列処理
// 複数の Result を並列で処理するユーティリティ
// all: 全て成功なら Ok、1つでも失敗なら最初の Err
function all<T, E>(results: Result<T, E>[]): Result<T[], E> {
const values: T[] = [];
for (const result of results) {
if (!result.ok) return result;
values.push(result.value);
}
return ok(values);
}
// allSettled: 全ての結果を収集
function allSettled<T, E>(
results: Result<T, E>[]
): { successes: T[]; failures: E[] } {
const successes: T[] = [];
const failures: E[] = [];
for (const result of results) {
if (result.ok) successes.push(result.value);
else failures.push(result.error);
}
return { successes, failures };
}
// 非同期版
async function allAsync<T, E>(
promises: Promise<Result<T, E>>[]
): Promise<Result<T[], E>> {
const results = await Promise.all(promises);
return all(results);
}
// 使用例
async function validateBulkUsers(
users: CreateUserDto[]
): Promise<Result<ValidatedUser[], ValidationError[]>> {
const results = users.map(user => validateUser(user));
const { successes, failures } = allSettled(results);
if (failures.length > 0) {
return err(failures);
}
return ok(successes);
}7.3 Result と Either の拡張
// Either 型: Result の一般化(Left/Right に意味付けしない)
type Either<L, R> = { tag: "left"; value: L } | { tag: "right"; value: R };
function left<L>(value: L): Either<L, never> {
return { tag: "left", value };
}
function right<R>(value: R): Either<never, R> {
return { tag: "right", value };
}
// bimap: 両方のケースを変換
function bimap<L, R, L2, R2>(
either: Either<L, R>,
leftFn: (l: L) => L2,
rightFn: (r: R) => R2,
): Either<L2, R2> {
if (either.tag === "left") return left(leftFn(either.value));
return right(rightFn(either.value));
}
// Validation 型: エラーを累積する Result
type Validation<T, E> = { ok: true; value: T } | { ok: false; errors: E[] };
function validateAll<T, E>(
validations: Validation<T, E>[]
): Validation<T[], E> {
const values: T[] = [];
const errors: E[] = [];
for (const v of validations) {
if (v.ok) {
values.push(v.value);
} else {
errors.push(...v.errors);
}
}
if (errors.length > 0) {
return { ok: false, errors };
}
return { ok: true, value: values };
}
// 使用例: フォームバリデーション(全エラーを一度に返す)
function validateRegistrationForm(data: FormData): Validation<ValidForm, FieldError> {
return validateAll([
validateName(data.name),
validateEmail(data.email),
validatePassword(data.password),
validateAge(data.age),
]).map(([name, email, password, age]) => ({
name, email, password, age,
}));
}
// Result.andThen は最初のエラーで短絡するが、
// Validation.validateAll は全てのエラーを収集する8. Scala と関数型言語での Result
8.1 Scala の Either と Try
// Scala: Either[L, R]
def divide(a: Double, b: Double): Either[String, Double] = {
if (b == 0) Left("Division by zero")
else Right(a / b)
}
// for 内包表記でチェーン(Haskell の do 記法に相当)
def calculate(a: Double, b: Double, c: Double): Either[String, Double] = {
for {
x <- divide(a, b) // Err なら即座に返る
y <- divide(x, c) // Err なら即座に返る
z <- if (y > 0) Right(y) else Left("Result must be positive")
} yield z * 100
}
// Try[T]: 例外を自動キャッチ
import scala.util.{Try, Success, Failure}
val result: Try[Int] = Try("42".toInt)
// Success(42)
val result: Try[Int] = Try("abc".toInt)
// Failure(java.lang.NumberFormatException)
val processed = Try("42".toInt)
.map(_ * 2)
.flatMap(n => if (n > 0) Success(n) else Failure(new Exception("negative")))
.recover { case _: NumberFormatException => 0 }
.getOrElse(-1)8.2 F# の Result
// F#: Result<'T, 'Error> は標準ライブラリの型
let divide a b : Result<float, string> =
if b = 0.0 then Error "Division by zero"
else Ok (a / b)
// パイプ演算子でチェーン
let processOrder orderId =
getOrder orderId
|> Result.bind validateOrder
|> Result.bind calculateTotal
|> Result.bind processPayment
|> Result.map createReceipt
// Computation Expression(do 記法に相当)
type ResultBuilder() =
member _.Bind(x, f) = Result.bind f x
member _.Return(x) = Ok x
let result = ResultBuilder()
let processOrder orderId = result {
let! order = getOrder orderId
let! validated = validateOrder order
let! total = calculateTotal validated
let! payment = processPayment total
return createReceipt payment
}9. テスト戦略
9.1 Result型のテスト
// Result型を使ったコードのテスト
describe("UserService.createUser", () => {
it("有効なデータで Ok(User) を返す", async () => {
const result = await userService.createUser({
name: "Test User",
email: "test@example.com",
password: "SecurePass123!",
});
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.value.name).toBe("Test User");
expect(result.value.email).toBe("test@example.com");
expect(result.value.id).toBeDefined();
}
});
it("無効なメールで Err(ValidationError) を返す", async () => {
const result = await userService.createUser({
name: "Test User",
email: "invalid-email",
password: "SecurePass123!",
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toBeInstanceOf(ValidationError);
expect(result.error.code).toBe("VALIDATION_ERROR");
}
});
it("重複メールで Err(ConflictError) を返す", async () => {
// 既存ユーザーを作成
await userService.createUser({
name: "Existing",
email: "existing@example.com",
password: "Pass123!",
});
const result = await userService.createUser({
name: "New User",
email: "existing@example.com",
password: "Pass123!",
});
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error.code).toBe("EMAIL_ALREADY_EXISTS");
}
});
});
// ヘルパー関数でテストを簡潔に
function expectOk<T, E>(result: Result<T, E>): T {
expect(result.ok).toBe(true);
if (!result.ok) throw new Error(`Expected Ok, got Err: ${result.error}`);
return result.value;
}
function expectErr<T, E>(result: Result<T, E>): E {
expect(result.ok).toBe(false);
if (result.ok) throw new Error(`Expected Err, got Ok: ${result.value}`);
return result.error;
}
// 使用例
it("ユーザーを正常に作成できる", async () => {
const user = expectOk(await userService.createUser(validData));
expect(user.name).toBe("Test User");
});
it("バリデーションエラーを返す", async () => {
const error = expectErr(await userService.createUser(invalidData));
expect(error.code).toBe("VALIDATION_ERROR");
});9.2 プロパティベーステスト
// fast-check でのプロパティベーステスト
import fc from 'fast-check';
describe("Result invariants", () => {
it("map の恒等法則: result.map(x => x) === result", () => {
fc.assert(
fc.property(fc.integer(), (n) => {
const result = ok(n);
const mapped = result.map(x => x);
expect(mapped).toEqual(result);
})
);
});
it("andThen の結合法則", () => {
fc.assert(
fc.property(fc.integer(), (n) => {
const f = (x: number) => ok(x * 2);
const g = (x: number) => ok(x + 1);
const left = ok(n).andThen(f).andThen(g);
const right = ok(n).andThen(x => f(x).andThen(g));
expect(left).toEqual(right);
})
);
});
it("parsePort は常に 1-65535 の範囲か Err を返す", () => {
fc.assert(
fc.property(fc.string(), (input) => {
const result = parsePort(input);
if (result.ok) {
expect(result.value).toBeGreaterThanOrEqual(1);
expect(result.value).toBeLessThanOrEqual(65535);
}
// Err の場合はバリデーションが正しく機能している
})
);
});
});10. 実務での導入パターン
10.1 段階的導入のロードマップ
Result型の段階的導入:
Phase 1: ユーティリティの準備
→ Result 型の定義(またはライブラリの選定)
→ ok(), err() ヘルパー関数
→ tryCatch, tryCatchAsync ユーティリティ
Phase 2: バリデーション層から導入
→ フォームバリデーション
→ API リクエストのバリデーション
→ 設定値のバリデーション
→ ← 最も効果が高く、リスクが低い
Phase 3: サービス層に拡大
→ ドメインロジックの戻り値を Result に
→ 例外との境界を明確化
→ レポジトリ層は例外のまま
Phase 4: API レスポンスとの統合
→ Controller で Result をレスポンスに変換
→ エラーレスポンスの統一
→ OpenAPI スキーマとの整合
Phase 5: チーム全体での採用
→ コーディング規約の更新
→ コードレビューガイドラインの整備
→ テストパターンの標準化
10.2 チームでの合意形成
Result型導入時の議論ポイント:
1. ライブラリの選定
→ neverthrow: 最も人気、ResultAsync あり
→ ts-results: 軽量、Rust 風
→ 自前実装: 柔軟だが保守コスト
→ 組み込みの union type: ライブラリ不要だが機能少
2. 例外との境界ルール
→ どのレイヤーから Result を使うか
→ 例外を Result に変換する場所
→ フレームワークとの接点
3. エラー型の設計
→ string vs カスタムエラークラス
→ エラーコード体系
→ エラーの詳細度
4. 既存コードとの共存
→ 段階的移行 vs 一括移行
→ アダプター層の設計
→ テストの移行戦略
実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 言語 | エラー手法 | 特徴 |
|---|---|---|
| Rust | Result<T, E> + ? | 最も洗練されたResult型 |
| Go | (value, error) | シンプルだが冗長 |
| TypeScript | Union型 / neverthrow | 型で表現可能 |
| Java | 例外(+ Optional) | checked exception |
| Python | 例外 | 型ヒントで補完 |
| Haskell | Either a b | 元祖、モナドによるチェーン |
| Scala | Either / Try | for 内包表記で簡潔 |
| F# | Result<'T, 'E> | Computation Expression |
| Kotlin | runCatching / Result | Java 互換 |
| Swift | throws + Result | 両方使える |
次に読むべきガイド
参考文献
- The Rust Programming Language. "Error Handling."
- Go Blog. "Error handling and Go." 2011.
- Wlaschin, S. "Railway Oriented Programming." F# for Fun and Profit.
- neverthrow. "Type-Safe Error Handling in TypeScript." GitHub.
- Bloch, J. "Effective Java." Item 71: Avoid unnecessary use of checked exceptions.
- Syme, D. et al. "The F# Component Design Guidelines."
- Kotlin Documentation. "Exceptions."
- Apple Developer Documentation. "Error Handling in Swift."