为什么分类
错误分类法给失败稳定的名称和含义。没有它,每个调用方都自行解释:重试 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);