示例说明
无差别重试会把永久错误放大成三倍延迟,不重试又浪费了瞬态故障的自愈机会。分类是分界线:超时、连接重置、限流类错误标记为可重试,校验失败、鉴权拒绝类是永久错误。错误消息只是初筛,业务错误应在源头就带 retryable 标记,分类函数兜底归并。
技术要点
- retryable 标记驱动重试决策
- 永久错误首轮即上抛不浪费预算
- 指数退避从基数开始逐次翻倍
代码解读
classify 把消息匹配到瞬态特征列表的对象附加标记,PermanentError 类自带不可重试属性。withRetry 循环里分类失败后判断:不可重试或已到最后一次都直接抛出,否则按 2 的次幂递增延迟。示例用随机数模拟七成瞬态失败率,验证最终收敛。
使用建议
错误分类应与错误码体系对齐而不是匹配消息文案。重试要配抖动防同步风暴。审计重试率,异常升高说明下游在持续退化。
代码示例
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);