# Fail Early Pattern

> 应用尽早失败（快速失败）模式，在最早可能的时间点检测并报告错误。涵盖使用 守卫子句进行输入验证、有意义的错误消息、断言函数，以及静默吞噬失败的反模式。 主要以 R 语言为例，并提供通用/多语言指导。适用于编写接受外部输入的函数、 在 CRAN 提交前添加输入验证、重构静默产生错误结果的代码、审查 PR 的错误处理 质量，或强化内部 API 对无效参数的防护。

- Skill: `pjt222/fail-early-pattern-4` (Agent Skill)
- Install (CLI): `npx skillmds@latest add pjt222/fail-early-pattern-4`
- Raw SKILL.md: https://api.skillmd.com/api/skills/pjt222/fail-early-pattern-4/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- License: MIT
- Author: pjt222 (https://skillmd.com/u/pjt222)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/pjt222/fail-early-pattern-4

---


# 尽早失败

如果某件事会失败，它应该尽早失败、尽可能响亮地失败、携带尽可能多的上下文。此技能固化了尽早失败模式：在系统边界验证输入，使用守卫子句在错误状态传播之前拒绝它，以及编写能回答*什么*失败了、*在哪里*、*为什么*以及*如何修复*的错误消息。

## 适用场景

- 编写或审查接受外部输入（用户数据、API 响应、文件内容）的函数
- 在 CRAN 提交前为包函数添加输入验证
- 重构静默产生错误结果而非报错的代码
- 审查 pull request 的错误处理质量
- 强化内部 API 对无效参数的防护

## 输入

- **必需**：需要应用该模式的函数或模块
- **必需**：信任边界的识别（外部数据进入的位置）
- **可选**：需要重构的现有错误处理代码
- **可选**：目标语言（默认：R；也适用于 Python、TypeScript、Rust）

## 步骤

### 第 1 步：识别信任边界

映射外部数据进入系统的位置。这些是需要验证的点：

- 公共 API 函数（R 包中的导出函数）
- 面向用户的参数
- 文件 I/O（读取配置、数据文件、用户上传）
- 网络响应（API 调用、数据库查询）
- 环境变量和系统配置

仅由你自己已验证代码调用的内部辅助函数通常不需要冗余验证。

**预期结果：** 列出不可信数据进入代码的入口点。

**失败处理：** 若边界不清晰，从日志或错误报告中向后追踪，找到错误数据首次进入的位置。

### 第 2 步：在入口点添加守卫子句

在每个公共函数顶部、任何工作开始之前验证输入。

**R（base）：**

```r
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——包的首选）：**

```r
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）：**

```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 步：编写有意义的错误消息

每条错误消息应回答四个问题：

1. **什么**失败了——哪个参数或操作
2. **在哪里**——函数名或上下文（使用 `cli::cli_abort` 自动提供）
3. **为什么**——期望的是什么，实际收到了什么
4. **如何修复**——当修复方法不明显时

**好的消息：**

```r
# 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)}}"
))
```

**差的消息：**

```r
stop("Error")                    # What failed? No idea
stop("Invalid input")           # Which input? What's wrong with it?
stop(paste("Error in step", i)) # No actionable information
```

**预期结果：** 错误消息是自文档化的——第一次看到错误的开发者无需阅读源代码就能诊断和修复。

**失败处理：** 审查最近三个错误报告。若有任何一个需要阅读源代码才能理解，其错误消息需要改进。

### 第 4 步：优先使用 stop() 而非 warning()

当函数无法产生正确结果时使用 `stop()`（或 `cli::cli_abort()`）。仅当函数仍能产生有意义的结果但调用者应了解某问题时使用 `warning()`。

**经验法则：** 若用户可能静默地得到错误答案，那就是 `stop()`，而非 `warning()`。

```r
# 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
# 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)
  }
  # ...
}
```

**预期结果：** 内部不变量被断言，这样 bug 能在违规位置立即浮现，而非三个函数调用之后出现神秘错误。

**失败处理：** 若 `stopifnot()` 消息太隐晦，改为带上下文的显式 `if/stop`。

### 第 6 步：重构反模式

识别并修复以下常见反模式：

**反模式 1：空的 tryCatch（吞噬错误）**

```r
# 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：用默认值掩盖错误输入**

```r
# 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 当作修复方法**

```r
# 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：通用异常处理器**

```r
# 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 步：验证尽早失败重构

运行测试套件以确认错误路径正常工作：

```r
# 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"))
```

```bash
# Run full test suite
Rscript -e "devtools::test()"
```

**预期结果：** 所有测试通过。错误路径测试确认错误输入触发了预期的错误消息。

**失败处理：** 若现有测试依赖于静默失败（例如对错误输入返回 NULL），更新它们以期待新的错误。

## 验证清单

- [ ] 每个公共函数在开始工作前验证其输入
- [ ] 错误消息回答：什么失败了、在哪里、为什么、以及如何修复
- [ ] `stop()` 用于会产生错误结果的条件
- [ ] `warning()` 仅用于降级但有效的结果
- [ ] 无空的 `tryCatch` 块静默吞噬错误
- [ ] 无将 `suppressWarnings()` 用作适当验证替代品的情况
- [ ] 无静默掩盖无效输入的默认值
- [ ] 内部不变量使用 `stopifnot()` 或显式断言
- [ ] 每个验证守卫都有对应的错误路径测试
- [ ] 重构后测试套件通过

## 常见问题

- **验证太深**：在信任边界（公共 API）验证，而不是在每个内部辅助函数中。过度验证增加噪音，影响性能。
- **缺少上下文的错误消息**：`"Invalid input"` 迫使调用者猜测。始终包含参数名称、期望的类型/范围和实际收到的值。
- **用 warning() 代替 stop()**：若函数在警告后返回无意义结果，调用者会静默得到错误答案。使用 `stop()` 让调用者决定如何处理。
- **在 tryCatch 中吞噬错误**：`tryCatch(..., error = function(e) NULL)` 隐藏 bug。若必须捕获，记录日志或重新抛出并添加上下文。
- **忘记 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](../../guides/setting-up-your-environment.md)。

## 相关技能

- `write-testthat-tests` - 编写验证错误路径的测试
- `review-pull-request` - 审查代码中缺失验证和静默失败的问题
- `review-software-architecture` - 在系统级评估错误处理策略
- `create-skill` - 按照 agentskills.io 标准创建新技能
- `security-audit-codebase` - 与输入验证重叠的安全审查

