状态与错误处理
工作流跑起来之后,两件事决定它能不能可靠落地。一是状态怎么存,确保崩溃重启后接着跑而不是从头再来。二是出错怎么办,是重试还是放弃,重试几次,间隔多久。这两块处理不好,要么丢数据,要么无限重试拖垮系统。这篇把状态持久化、重试、超时、休眠四个机制讲透,配套 Cloudflare Workflows 的实际用法。
状态如何持久化
工作流的状态持久化是自动的,不需要你手动写数据库。核心规则只有一条,每个 step.do 回调返回的值会被引擎存下来,下次重跑到这个 Step 直接读存档跳过执行。
状态来源对比
| 状态来源 | 是否持久化 | 能否在后续 Step 使用 |
|---|---|---|
| step.do 返回值 | 是 | 能,作为返回值接收 |
| event.payload | 否,不可变 | 能读取,但修改无效 |
| run 局部变量 | 否 | 仅当次执行有效 |
| this.env 绑定 | 外部资源 | 每次实时读取 |
event.payload 看起来像状态,其实它在整个工作流期间是不可变的。你在某个 Step 里改了 event.payload 的字段,下一个 Step 读到的还是原始值。想在步骤间传数据,只能靠 step.do 的返回值。
export class OrderWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// 第一步返回订单数据,自动持久化
const order = await step.do("load order", async () => {
const row = await this.env.DB
.prepare("SELECT * FROM orders WHERE id = ?")
.bind(event.payload.orderId)
.first();
return row;
});
// 第二步用上一步的返回值,order 已存档
const paid = await step.do("charge", async () => {
return { orderId: order.id, amount: order.total };
});
// run 的返回值也会持久化,可通过 status 查到
return { orderId: order.id, paid: paid.amount };
}
}
run 方法的返回值同样会被存下来,查询实例状态时能看到 output 字段。这对后续校验、对账很有用。
存档的代价是单步返回值必须可序列化,且不超过 1 MiB。大文件别直接返回,存到 R2(Cloudflare 的对象存储服务)后把键名返回就行。
重试机制
每个 step.do 可以单独配置重试策略。不配置时走默认值。
默认重试配置
| 参数 | 默认值 | 说明 |
|---|---|---|
| limit | 5 | 最大重试次数 |
| delay | 10000 毫秒 | 每次重试间隔 |
| backoff | exponential | 退避算法 |
| timeout | 10 minutes | 单次执行超时 |
自定义重试时把配置对象作为 step.do 的第二个参数传入。
const result = await step.do(
"call payment api",
{
retries: {
limit: 10,
delay: "5 seconds",
backoff: "exponential",
},
timeout: "1 minute",
},
async () => {
const res = await fetch("https://api.payment.com/charge", {
method: "POST",
body: JSON.stringify({ amount: 100 }),
});
if (!res.ok) throw new Error("支付接口失败");
return await res.json();
},
);
退避算法决定间隔如何随重试次数增长。
三种退避算法对比
| 算法 | 间隔增长方式 | 适用场景 |
|---|---|---|
| constant | 固定不变 | 轻微抖动,快速重试 |
| linear | 线性增长 | 适中压力,温和退避 |
| exponential | 指数增长 | 高负载服务,避免压垮下游 |
举例说明,limit 为 5,delay 为 10 秒时
| 重试次数 | constant | linear | exponential |
|---|---|---|---|
| 第 1 次 | 10 秒 | 10 秒 | 10 秒 |
| 第 2 次 | 10 秒 | 20 秒 | 20 秒 |
| 第 3 次 | 10 秒 | 30 秒 | 40 秒 |
| 第 4 次 | 10 秒 | 40 秒 | 80 秒 |
| 第 5 次 | 10 秒 | 50 秒 | 160 秒 |
exponential 退避最常用,能快速给下游减压。单 Step 重试上限 10000 次,正常业务配 3 到 10 次就够。
需要根据失败原因动态调整间隔时,delay 可以传一个函数。
await step.do(
"sync customer",
{
retries: {
limit: 5,
delay: ({ ctx, error }) => {
// 限流错误拉长间隔
if (error.message.includes("rate limit")) {
return `${ctx.attempt * 30} seconds`;
}
return "10 seconds";
},
},
},
async () => {
await syncCustomer();
},
);
delay 函数接收一个对象,ctx.attempt 是当前重试序号,error 是触发的错误。返回时长字符串或毫秒数都行。
超时控制
timeout 控制的是单次执行的最大时长,不是整个 Step 的总时长。一次执行超时就算一次失败,触发重试。
超时配置要点
| 配置项 | 作用 | 默认值 |
|---|---|---|
| timeout | 单次尝试最长跑多久 | 10 minutes |
| 触发后 | 计入失败次数,进入重试流程 | |
| 配合 limit | limit 次都超时则整个 Step 失败 |
await step.do(
"long fetch",
{
retries: { limit: 3, delay: "10 seconds", backoff: "linear" },
timeout: "30 seconds",
},
async () => {
const res = await fetch("https://slow-api.example.com/data");
return await res.json();
},
);
这里每次尝试最多跑 30 秒,超时后等 10 秒再试,最多试 3 次。3 次都失败,整个 Step 标记失败,工作流终止。
timeout 要结合下游实际响应时间来配。太短会误杀正常慢请求,太长会拖慢整体故障恢复。
休眠与定时
工作流可以主动休眠,把执行暂停一段时间后再继续。这是工作流区别于普通 Worker 的重要能力,让长耗时任务成为可能。
两种休眠方法
| 方法 | 作用 | 第二个参数 |
|---|---|---|
| step.sleep | 休眠一段相对时长 | 时长字符串或毫秒 |
| step.sleepUntil | 休眠到某个固定时刻 | Date 或时间戳 |
await step.sleep("等一会", "10 seconds");
await step.sleep("等久点", "1 hour");
await step.sleep("纯数字也行", 60000);
sleep 的时长支持人类可读字符串,接受的单位如下
| 单位 | 含义 |
|---|---|
| second | 秒 |
| minute | 分 |
| hour | 小时 |
| day | 天 |
| week | 周 |
| month | 月 |
| year | 年 |
sleepUntil 适合定点执行的场景,比如等到下周日早上九点再跑。
// 等到固定日期
const launchTime = Date.parse("2026-12-25 09:00:00 UTC");
await step.sleepUntil("等到产品发布", launchTime);
休眠期间工作流实例不占计算资源,引擎只记一个唤醒时间。这意味着工作流可以休眠几小时甚至几天,成本极低。
一个典型场景是订单确认窗口。下单后扣款,休眠 24 小时等用户确认收货,再触发后续流程。
export class OrderWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const order = await step.do("create order", async () => {
return { orderId: event.payload.orderId, total: 199 };
});
await step.do("charge", async () => {
return { charged: true };
});
// 休眠 24 小时等收货
await step.sleep("收货确认窗口", "24 hours");
const confirmed = await step.do("check receipt", async () => {
return { delivered: true };
});
return { orderId: order.orderId, delivered: confirmed.delivered };
}
}
不可重试错误
有些错误重试多少次都没用,比如认证失败、参数格式错误。默认情况下任何抛出的错误都会触发重试,会浪费资源。这时用 NonRetryableError 强制让工作流失败且不重试。
import {
WorkflowEntrypoint,
WorkflowStep,
NonRetryableError,
} from "cloudflare:workers";
import type { WorkflowEvent } from "cloudflare:workers";
export class MyWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
const order = await step.do("load order", async () => {
const row = await this.env.DB
.prepare("SELECT * FROM orders WHERE id = ?")
.bind(event.payload.orderId)
.first();
// 订单不存在,重试无意义,直接终止
if (!row) {
throw new NonRetryableError("订单不存在");
}
return row;
});
return order;
}
}
普通 Error 和 NonRetryableError 的区别
| 错误类型 | 是否重试 | 适用场景 |
|---|---|---|
| Error | 是 | 瞬时故障,网络抖动、临时超时 |
| NonRetryableError | 否 | 永久错误,数据不存在、认证失败 |
判断标准很简单,问自己这个错误再试一次会不会成功。会就抛 Error,不会就抛 NonRetryableError。
错误处理实战
把重试、超时、不可重试组合起来,覆盖大部分生产场景。下面是一个订单处理工作流的完整错误处理。
import {
WorkflowEntrypoint,
WorkflowStep,
NonRetryableError,
} from "cloudflare:workers";
import type { WorkflowEvent } from "cloudflare:workers";
type Params = { orderId: string };
export class OrderWorkflow extends WorkflowEntrypoint<Env, Params> {
async run(event: WorkflowEvent<Params>, step: WorkflowStep) {
// 加载订单,不存在直接失败不重试
const order = await step.do("load order", async () => {
const row = await this.env.DB
.prepare("SELECT * FROM orders WHERE id = ?")
.bind(event.payload.orderId)
.first();
if (!row) throw new NonRetryableError("订单不存在");
return row;
});
// 扣款,支付接口可能抖动,重试 5 次
const paid = await step.do(
"charge card",
{
retries: { limit: 5, delay: "10 seconds", backoff: "exponential" },
timeout: "30 seconds",
},
async () => {
const res = await fetch("https://api.payment.com/charge", {
method: "POST",
body: JSON.stringify({ amount: order.total }),
});
// 余额不足是永久错误
if (res.status === 402) {
throw new NonRetryableError("余额不足");
}
if (!res.ok) throw new Error("支付接口异常");
return await res.json();
},
);
// 休眠等结算
await step.sleep("结算窗口", "1 hour");
const settled = await step.do("settle", async () => {
return { orderId: order.id, settled: true };
});
return { orderId: order.id, paid: paid.id, settled: settled.settled };
}
}
常见现象与对策
| 现象 | 调整方向 |
|---|---|
| 接口偶发抖动失败 | 加大 limit,用 exponential 退避 |
| 下游限流返回 429 | delay 改动态函数,按限流响应拉长间隔 |
| 参数错误无限重试 | 抛 NonRetryableError |
| 慢接口频繁超时 | 加大 timeout,减少 limit |
| 重试间隔太短压垮下游 | 改 exponential 或加大 delay |
| 业务永久失败 | NonRetryableError 直接终止 |
小结
状态持久化靠 step.do 的返回值,event 不可变,传数据用返回值不用 event。重试三参数 limit、delay、backoff 加 timeout 控制单次时长,默认值能覆盖大多数场景,特殊需求按现象调。永久错误用 NonRetryableError 终止重试,避免无谓消耗。休眠让工作流能跑几小时几天,成本几乎为零。下一篇讲条件分支和并行执行,让工作流处理更复杂的业务编排。