fail-early-pattern
について
このスキルは、入力の早期検証と明確なエラーメッセージの提供を通じてフェイルファーストパターンを実装し、ガード節、アサーション関数、アンチパターンを網羅しています。外部向け関数の作成、コードリファクタリング、エラー処理のレビューといったシナリオ向けに、Rのサンプルコードと多言語ガイダンスを含んでいます。開発者は、エラーを黙って伝播させるのではなく、意味のあるコンテキストと共に即座に失敗させる必要がある堅牢な関数を構築する際に、これを活用すべきです。
クイックインストール
Claude Code
推奨npx skills add pjt222/agent-almanac -a claude-code/plugin add https://github.com/pjt222/agent-almanacgit clone https://github.com/pjt222/agent-almanac.git ~/.claude/skills/fail-early-patternこのコマンドをClaude Codeにコピー&ペーストしてスキルをインストールします
ドキュメント
フェイルアーリー
何かが失敗する場合、できるだけ早く、できるだけ大きな声で、できるだけ多くのコンテキストとともに失敗すべきだ。このスキルはフェイルアーリーパターンを体系化する: システム境界で入力を検証し、ガード句を使用して悪い状態が伝播する前に拒否し、何が失敗したか、どこで、なぜ、修正方法に答えるエラーメッセージを書く。
使用タイミング
- 外部入力(ユーザーデータ、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 では、
Rscriptがネイティブ R の代わりにクロスプラットフォームラッパーに解決される場合があります。which Rscript && Rscript --versionで確認してください。信頼性のために、ネイティブ R バイナリ(例:Linux/WSL では/usr/local/bin/Rscript)を優先してください。R パス設定については Setting Up Your Environment を参照してください。
関連スキル
write-testthat-tests- エラーパスを確認するテストを書くreview-pull-request- 欠けているバリデーションと黙った失敗のコードをレビューするreview-software-architecture- システムレベルでエラーハンドリング戦略を評価するcreate-skill- agentskills.io標準に従って新しいスキルを作成するsecurity-audit-codebase- 入力バリデーションと重複するセキュリティ重視のレビュー
GitHub リポジトリ
関連スキル
qmd
開発qmdは、BM25、ベクトル埋め込み、およびリランキングを組み合わせたハイブリッド検索を用いて、ローカルファイルのインデックス作成と検索を可能にするローカル検索・インデックス作成CLIツールです。コマンドラインでの使用と、Claudeとの統合のためのMCP(Model Context Protocol)モードの両方をサポートしています。このツールは埋め込みにOllamaを使用し、インデックスをローカルに保存するため、ターミナルから直接ドキュメントやコードベースを検索するのに最適です。
subagent-driven-development
開発このスキルは、各独立したタスクに対して新規のサブエージェントを起動し、タスク間でコードレビューを実施しながら実装計画を実行します。レビュープロセスを通じて品質基準を維持しつつ、迅速な反復を可能にします。同一セッション内で主に独立したタスクに取り組む際に本スキルをご利用いただくことで、組み込まれた品質チェックを伴う継続的な進捗を確保できます。
mcporter
開発mcporterスキルは、開発者がClaudeから直接Model Context Protocol(MCP)サーバーを管理および呼び出せるようにします。このスキルは、利用可能なサーバーの一覧表示、引数を指定したツールの呼び出し、認証およびデーモンのライフサイクル管理を行うコマンドを提供します。開発ワークフローにおいてMCPサーバーの機能を統合およびテストする際に、このスキルをご利用ください。
adk-deployment-specialist
開発このスキルは、A2Aプロトコルを使用してVertex AI ADKエージェントをデプロイおよびオーケストレーションし、AgentCardの発見、タスク送信、およびコード実行サンドボックスやメモリバンクなどのサポートツールを管理します。Python、Java、またはGoで、順次、並列、またはループのオーケストレーションパターンを用いたマルチエージェントシステムの構築を可能にします。Google Cloud上でADKエージェントのデプロイやエージェントワークフローのオーケストレーションを求められた際にご利用ください。
