← 中文资源

简体中文版本

错误分类法(Error Taxonomy)

错误分类法按原因、可重试性和责任归属划分失败,让客户端、运维和仪表盘以一致方式处理错误而不是各自猜测。

阅读英文原文

为什么分类

错误分类法给失败稳定的名称和含义。没有它,每个调用方都自行解释:重试 500、忽略 400、对 409 呼叫。分类法一致回答三个问题:发生了什么类型的失败、重试是否有帮助、谁负责解决。它把错误处理从猜测变成服务、客户端和运维共享的契约。

有用的维度

按原因分类:校验、认证、授权、未找到、冲突、限流、依赖失败、超时和内部缺陷。按可重试性分类:可安全重试、延迟后重试、使用新幂等键重试、永不重试。按责任边界分类:调用方错误、服务错误或依赖错误。增加稳定的机器可读代码、人类可读消息和可选的结构化详情。例如,支付被拒和支付提供者故障都是失败,但只有一种可以重试。

设计规则

用传输层状态码表达粗粒度语义,用领域代码表达精确含义。保持代码稳定;改变含义会静默破坏客户端。不要暴露堆栈、SQL 或内部主机名。包含用于支持的关联标识符,绝不包含密钥或个人数据。记录哪些代码可重试以及使用何种退避。语义必须变化时,对分类法做版本化。

处理、可观测性与局限

客户端应基于代码而非消息文本分支。网关可以把依赖失败映射为一致代码。仪表盘按类别和负责人分组,避免把校验错误激增误判为故障。对表示服务缺陷的类别告警,并在日志和 trace 中使用同一分类法。分类法无法修复模糊失败:超时可能掩盖一次成功写入,这正是幂等性重要的原因。与松散的消息集合相比,分类法增加前期设计成本,却减少集成缺陷并加快诊断。

代码示例

#!/usr/bin/env bash
set -euo pipefail

# Custom exit codes
E_SUCCESS=0
E_NO_ARGS=1
E_FILE_NOT_FOUND=2
E_PERMISSION=3
E_DEPENDENCY=4

# Check dependencies
check_dependency() {
    if ! command -v "$1" &>/dev/null; then
        echo "Error: $1 is required but not installed" >&2
        exit "$E_DEPENDENCY"
    fi
}

# Main function with error handling
main() {
    local input_file="${1:-}"

    if [[ -z "$input_file" ]]; then
        echo "Usage: $0 <input-file>" >&2
        exit "$E_NO_ARGS"
    fi

    if [[ ! -f "$input_file" ]]; then
        echo "Error: File not found: $input_file" >&2
        exit "$E_FILE_NOT_FOUND"
    fi

    if [[ ! -r "$input_file" ]]; then
        echo "Error: Cannot read file: $input_file" >&2
        exit "$E_PERMISSION"
    fi

    local output
    if ! output=$(wc -l "$input_file" 2>&1); then
        echo "Error processing file: $output" >&2
        exit 1
    fi

    echo "$output"
    exit "$E_SUCCESS"
}

# Check required tools
for tool in grep sort wc; do
    check_dependency "$tool"
done

main "$@"
interface RetryableError extends Error {
  retryable?: boolean;
}

class PermanentError extends Error {
  retryable = false;
}

function classify(err: unknown): RetryableError {
  if (err instanceof PermanentError) return err;

  const message = err instanceof Error ? err.message : String(err);
  const transient =
    /timeout|econnreset|socket hang up|503|429/i.test(message);

  return Object.assign(
    err instanceof Error ? err : new Error(message),
    { retryable: transient },
  );
}

async function withRetry<T>(
  fn: () => Promise<T>,
  { attempts = 3, baseMs = 200 } = {},
): Promise<T> {
  let lastError: RetryableError;

  for (let i = 1; i <= attempts; i++) {
    try {
      return await fn();
    } catch (raw) {
      lastError = classify(raw);

      if (!lastError.retryable || i === attempts) {
        throw lastError;
      }
      await new Promise((r) => setTimeout(r, baseMs * 2 ** (i - 1)));
    }
  }
  throw lastError!;
}

const result = await withRetry(async () => {
  if (Math.random() < 0.7) {
    const err = new Error('upstream 503 unavailable');
    throw err;
  }
  return 'settled';
});
console.log('outcome:', result);