Agent-almanac fail-early-pattern
git clone https://github.com/pjt222/agent-almanac
T=$(mktemp -d) && git clone --depth=1 https://github.com/pjt222/agent-almanac "$T" && mkdir -p ~/.claude/skills && cp -r "$T/i18n/ja/skills/fail-early-pattern" ~/.claude/skills/pjt222-agent-almanac-fail-early-pattern-69f903 && rm -rf "$T"
i18n/ja/skills/fail-early-pattern/SKILL.mdフェイルアーリー
何かが失敗する場合、できるだけ早く、できるだけ大きな声で、できるだけ多くのコンテキストとともに失敗すべきだ。このスキルはフェイルアーリーパターンを体系化する: システム境界で入力を検証し、ガード句を使用して悪い状態が伝播する前に拒否し、何が失敗したか、どこで、なぜ、修正方法に答えるエラーメッセージを書く。
使用タイミング
- 外部入力(ユーザーデータ、APIレスポンス、ファイルコンテンツ)を受け付ける関数を書く/レビューする場合
- CRAN申請前にパッケージ関数に入力バリデーションを追加する場合
- エラーを出す代わりに黙って誤った結果を生成するコードをリファクタリングする場合
- エラーハンドリング品質のプルリクエストをレビューする場合
- 無効な引数に対して内部APIを強化する場合
入力
- 必須: パターンを適用する関数またはモジュール
- 必須: トラスト境界の特定(外部データが入ってくる場所)
- 任意: リファクタリングする既存のエラーハンドリングコード
- 任意: ターゲット言語(デフォルト: R; Python、TypeScript、Rustにも適用)
手順
ステップ1: トラスト境界を特定する
外部データがシステムに入ってくる場所をマップする。これらがバリデーションが必要なポイントだ:
- パブリックAPI関数(Rパッケージのエクスポートされた関数)
- ユーザー向けパラメータ
- ファイルI/O(設定、データファイル、ユーザーアップロードの読み込み)
- ネットワークレスポンス(APIコール、データベースクエリ)
- 環境変数とシステム設定
検証済みのコードのみによって呼ばれる内部ヘルパー関数は、通常冗長なバリデーションを必要としない。
期待結果: 信頼されていないデータがコードに入ってくるエントリポイントのリスト。
失敗時: 境界が不明確な場合、ログや不具合レポートのエラーから後ろ向きに追跡して、不正なデータが最初に入ってきた場所を見つける。
ステップ2: エントリポイントにガード句を追加する
各パブリック関数の先頭で、作業が始まる前に入力を検証する。
R(base):
calculate_summary <- function(data, method = c("mean", "median", "trim"), trim_pct = 0.1) { # Guard: type check if (!is.data.frame(data)) { stop("'data' must be a data frame, not ", class(data)[[1]], call. = FALSE) } # Guard: non-empty if (nrow(data) == 0L) { stop("'data' must have at least one row", call. = FALSE) } # Guard: argument matching method <- match.arg(method) # Guard: range check if (!is.numeric(trim_pct) || trim_pct < 0 || trim_pct > 0.5) { stop("'trim_pct' must be a number between 0 and 0.5, got: ", trim_pct, call. = FALSE) } # --- All guards passed, begin real work --- # ... }
R(rlang/cli — パッケージに推奨):
calculate_summary <- function(data, method = c("mean", "median", "trim"), trim_pct = 0.1) { rlang::check_required(data) if (!is.data.frame(data)) { cli::cli_abort("{.arg data} must be a data frame, not {.cls {class(data)}}.") } if (nrow(data) == 0L) { cli::cli_abort("{.arg data} must have at least one row.") } method <- rlang::arg_match(method) if (!is.numeric(trim_pct) || trim_pct < 0 || trim_pct > 0.5) { cli::cli_abort("{.arg trim_pct} must be between 0 and 0.5, not {.val {trim_pct}}.") } # ... }
一般(TypeScript):
function calculateSummary(data: DataFrame, method: Method, trimPct: number): Summary { if (data.rows.length === 0) { throw new Error(`data must have at least one row`); } if (trimPct < 0 || trimPct > 0.5) { throw new RangeError(`trimPct must be between 0 and 0.5, got: ${trimPct}`); } // ... }
期待結果: すべてのパブリック関数が、副作用や計算の前に無効な入力を拒否するガード句で始まる。
失敗時: バリデーションロジックが長くなりすぎる場合(ガードが15行以上)、
validate_* ヘルパーを抽出するか、単純な型アサーションには stopifnot() を使用する。
ステップ3: 意味のあるエラーメッセージを書く
すべてのエラーメッセージは4つの質問に答えるべきだ:
- 何が失敗したか — どのパラメータまたは操作
- どこで — 関数名またはコンテキスト(
で自動)cli::cli_abort - なぜ — 期待されたものと受け取ったものの比較
- 修正方法 — 修正が非自明な場合
良いメッセージ:
# What + Why (expected vs. actual) stop("'n' must be a positive integer, got: ", n, call. = FALSE) # What + Why + How to fix cli::cli_abort(c( "{.arg config_path} does not exist: {.file {config_path}}", "i" = "Create it with {.run create_config({.file {config_path}})}." )) # What + context cli::cli_abort(c( "Column {.val {col_name}} not found in {.arg data}.", "i" = "Available columns: {.val {names(data)}}" ))
悪いメッセージ:
stop("Error") # 何が失敗した? わからない stop("Invalid input") # どの入力? 何が問題? stop(paste("Error in step", i)) # 実行可能な情報なし
期待結果: エラーメッセージが自己文書化されている — エラーを初めて見た開発者がソースコードを読まずに診断して修正できる。
失敗時: 最近の3つの不具合レポートを確認する。ソースコードを読む必要があった場合、そのエラーメッセージは改善が必要だ。
ステップ4: warning()よりstop()を優先する
関数が正しい結果を生成できない場合は
stop()(または cli::cli_abort())を使用する。関数がまだ意味のある結果を生成できるが呼び出し元が懸念を知るべき場合にのみ warning() を使用する。
経験則: ユーザーが黙って誤った回答を得る可能性がある場合、それは
stop() であり、warning() ではない。
# CORRECT: stop when result would be wrong read_config <- function(path) { if (!file.exists(path)) { stop("Config file not found: ", path, call. = FALSE) } yaml::read_yaml(path) } # CORRECT: warn when result is still usable summarize_data <- function(data) { if (any(is.na(data$value))) { warning(sum(is.na(data$value)), " NA values dropped from 'value' column", call. = FALSE) data <- data[!is.na(data$value), ] } # proceed with valid data }
期待結果:
stop() が誤った結果を生成する条件に使用され、warning() が劣化しているが有効な結果に予約されている。
失敗時: 既存の
warning() 呼び出しを監査する。警告後に関数がナンセンスを返す場合、stop() に変更する。
ステップ5: 内部不変条件にアサーションを使用する
「正しいコードでは絶対に起こらない」条件にはアサーションを使用する。これらは開発中のプログラマーエラーをキャッチする:
# R: stopifnot for internal invariants process_chunk <- function(chunk, total_size) { stopifnot( is.list(chunk), length(chunk) > 0, total_size > 0 ) # ... } # R: explicit assertion with context merge_results <- function(left, right) { if (ncol(left) != ncol(right)) { stop("Internal error: column count mismatch (", ncol(left), " vs ", ncol(right), "). This is a bug — please report it.", call. = FALSE) } # ... }
期待結果: 内部不変条件がアサートされ、3つの関数呼び出し後に不可解なエラーではなく、違反サイトですぐにバグが表面化する。
失敗時:
stopifnot() メッセージが不可解すぎる場合、コンテキストを含む明示的な if/stop に切り替える。
ステップ6: アンチパターンをリファクタリングする
これらの一般的なアンチパターンを特定して修正する:
アンチパターン1: 空のtryCatch(エラーを飲み込む)
# BEFORE: Error silently disappears result <- tryCatch( parse_data(input), error = function(e) NULL ) # AFTER: Log, re-throw, or return a typed error result <- tryCatch( parse_data(input), error = function(e) { cli::cli_abort("Failed to parse input: {e$message}", parent = e) } )
アンチパターン2: 悪い入力を隠すデフォルト値
# BEFORE: Caller never knows their input was ignored process <- function(x = 10) { if (!is.numeric(x)) x <- 10 # silently replaces bad input x * 2 } # AFTER: Tell the caller about the problem process <- function(x = 10) { if (!is.numeric(x)) { stop("'x' must be numeric, got ", class(x)[[1]], call. = FALSE) } x * 2 }
アンチパターン3: suppressWarningsを修正として使用
# BEFORE: Hiding the symptom instead of fixing the cause result <- suppressWarnings(as.numeric(user_input)) # AFTER: Validate explicitly, handle the expected case if (!grepl("^-?\\d+\\.?\\d*$", user_input)) { stop("Expected a number, got: '", user_input, "'", call. = FALSE) } result <- as.numeric(user_input)
アンチパターン4: キャッチオール例外ハンドラー
# BEFORE: Every error treated the same tryCatch( complex_operation(), error = function(e) message("Something went wrong") ) # AFTER: Handle specific conditions, let unexpected ones propagate tryCatch( complex_operation(), custom_validation_error = function(e) { cli::cli_warn("Validation issue: {e$message}") fallback_value } # Unexpected errors propagate naturally )
期待結果: アンチパターンが明示的なバリデーションまたは特定のエラーハンドリングに置き換えられる。
失敗時:
tryCatch を削除すると連鎖的な失敗が発生する場合、上流のコードにバリデーションギャップがある。症状ではなく原因を修正する。
ステップ7: フェイルアーリーリファクタリングを検証する
テストスイートを実行してエラーパスが正しく機能することを確認する:
# Verify error messages are triggered testthat::expect_error(calculate_summary("not_a_df"), "must be a data frame") testthat::expect_error(calculate_summary(data.frame()), "at least one row") testthat::expect_error(calculate_summary(mtcars, trim_pct = 2), "between 0 and 0.5") # Verify valid inputs still work testthat::expect_no_error(calculate_summary(mtcars, method = "mean"))
# Run full test suite Rscript -e "devtools::test()"
期待結果: すべてのテストがパスする。エラーパスのテストが、悪い入力が期待されるエラーメッセージをトリガーすることを確認する。
失敗時: 既存のテストが黙った失敗(例: 悪い入力でNULLを返す)に依存していた場合、新しいエラーを期待するように更新する。
バリデーション
- すべてのパブリック関数が作業前に入力を検証する
- エラーメッセージが答える: 何が失敗したか、どこで、なぜ、修正方法
-
が誤った結果を生成する条件に使用されているstop() -
が劣化しているが有効な結果にのみ使用されているwarning() - エラーを黙って飲み込む空の
ブロックがないtryCatch - 適切なバリデーションの代替として
が使用されていないsuppressWarnings() - 無効な入力を黙って隠すデフォルト値がない
- 内部不変条件が
または明示的なアサーションを使用するstopifnot() - 各バリデーションガードのエラーパステストが存在する
- リファクタリング後にテストスイートがパスする
よくある落とし穴
-
深すぎる場所でのバリデーション: トラスト境界(パブリックAPI)でバリデーションを行い、すべての内部ヘルパーではない。過剰なバリデーションはノイズを追加しパフォーマンスを損なう。
-
コンテキストのないエラーメッセージ:
は呼び出し元に推測を強いる。常にパラメータ名、期待される型/範囲、受け取った実際の値を含める。"Invalid input" -
stop()の代わりにwarning()を使用: 警告後に関数がゴミを返す場合、呼び出し元は黙って誤った回答を得る。
を使用して呼び出し元が処理方法を決めるようにする。stop() -
tryCatchでエラーを飲み込む:
がバグを隠す。キャッチする必要がある場合、コンテキストを追加してログに記録するか再スローする。tryCatch(..., error = function(e) NULL) -
call. = FALSEを忘れる: Rでは
はデフォルトでコールを含み、エンドユーザーにはノイズになる。ユーザー向け関数ではstop("msg")
を使用する。call. = FALSE
はこれを自動的に行う。cli::cli_abort() -
コードではなくテストでバリデーション: テストは動作を確認するが、本番の呼び出し元を保護しない。バリデーションは関数自体に属する。
-
ハイブリッドシステムでの誤った R バイナリ:WSL や Docker では、
がネイティブ R の代わりにクロスプラットフォームラッパーに解決される場合があります。Rscript
で確認してください。信頼性のために、ネイティブ R バイナリ(例:Linux/WSL ではwhich Rscript && Rscript --version
)を優先してください。R パス設定については Setting Up Your Environment を参照してください。/usr/local/bin/Rscript
関連スキル
- エラーパスを確認するテストを書くwrite-testthat-tests
- 欠けているバリデーションと黙った失敗のコードをレビューするreview-pull-request
- システムレベルでエラーハンドリング戦略を評価するreview-software-architecture
- agentskills.io標準に従って新しいスキルを作成するcreate-skill
- 入力バリデーションと重複するセキュリティ重視のレビューsecurity-audit-codebase