处理流程失败、超时与重试
只重试明确可恢复的只读步骤,传递取消信号,保留失败身份,避免重复执行副作用。
处理流程失败、超时与重试
一个步骤失败后,是停止整个流程、保留部分结果,还是重新执行,需要由程序决定。把整个工作流放进一个无限重试循环,会让已经完成的动作反复发生。
本篇只给可重试的只读步骤增加有限重试。使用 Node 自带的计时器,不需要模型服务。
先区分三种情况
“错误消息里包含 timeout”不是可靠分类器。本例用明确的错误类型演示重试决策,实际接入 HTTP 或 SDK 时需要根据其状态和契约分类。
限制尝试次数并传递信号
创建 src/retry.ts:
import { setTimeout as delay } from "node:timers/promises"
export class TransientStepError extends Error {}
export async function retryRead<T>(
execute: (signal: AbortSignal) => Promise<T>,
signal: AbortSignal,
maxAttempts = 2,
): Promise<T> {
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
throw new Error("最大尝试次数必须是正整数")
}
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
signal.throwIfAborted()
try {
const value = await execute(signal)
signal.throwIfAborted()
return value
} catch (error) {
if (signal.aborted || !(error instanceof TransientStepError) || attempt === maxAttempts) {
throw error
}
await delay(100 * attempt, undefined, { signal })
}
}
throw new Error("没有得到结果")
}只有 TransientStepError 会继续尝试,普通错误立即抛出。等待也接收同一个信号,用户取消时不会睡满时间后再请求一次。
参数叫 maxAttempts,包含第一次执行,不是额外重试次数。与底层 SDK 叠加时,还需要检查 SDK 自身的重试策略,否则表面两次调用可能对应更多网络请求。
运行一份确定的失败样本
创建 src/failure-main.ts:
import { retryRead, TransientStepError } from "./retry.js"
let calls = 0
const signal = AbortSignal.timeout(2_000)
try {
const value = await retryRead(async () => {
calls += 1
if (calls === 1) {
throw new TransientStepError("教学模拟:第一次只读请求暂时失败")
}
return { source: "fixture", checked: true }
}, signal)
console.log({ status: "completed", calls, value })
} catch (error) {
console.error({ status: signal.aborted ? "cancelled" : "failed", calls, error: String(error) })
process.exitCode = 1
}运行:
node --import tsx src/failure-main.ts预期显示两次尝试后完成。第一次失败是程序主动制造的教学故障,不是声称恢复了真实服务。把异常换成普通 Error,则应第一次就失败。
示例把信号中止统一归入 cancelled,它可能来自时间上限,也可能来自用户操作。产品中如果需要分别展示,应继续记录并判断中止原因,不把所有中止都写成用户主动取消。
保留步骤身份,不从头重做
在更长的流程里,错误至少应关联步骤名称、尝试次数和任务身份。前面已经完成的读取或分析结果可以在本次运行中保留,但内存里的变量不等于退出后仍能恢复。
并行流程可以继续使用带身份的结果数组;顺序流程应停止依赖失败步骤的下游动作。不要为了产出一份“完整报告”而用空数据代替失败输入。
为什么不把这个函数用于任意写入
发送消息、保存文件和调用外部业务接口可能已经生效,只是响应丢失。此时简单重试会产生重复操作。要先确认动作是否允许重做、是否有幂等键,以及怎样核对结果未知的情况。
AbortSignal 也不是事务回滚。已经完成的动作不会因为后续取消而撤销,忽略信号的第三方代码可能继续运行。因此调用者既要发送取消,还要等待受管理的工作结束。
下一篇实现一个明确范围内的任务保存与继续,实际观察进程退出后的恢复和写入去重。