← 中文资源

简体中文版本

异步错误分类重试策略

把异步失败分类为可重试与永久两类:仅瞬态错误按指数退避重试,永久错误立即上抛,重试预算耗尽后透传最后一次错误。

阅读英文原文

示例说明

无差别重试会把永久错误放大成三倍延迟,不重试又浪费了瞬态故障的自愈机会。分类是分界线:超时、连接重置、限流类错误标记为可重试,校验失败、鉴权拒绝类是永久错误。错误消息只是初筛,业务错误应在源头就带 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);