六维教程

状态与错误处理

工作流跑起来之后,两件事决定它能不能可靠落地。一是状态怎么存,确保崩溃重启后接着跑而不是从头再来。二是出错怎么办,是重试还是放弃,重试几次,间隔多久。这两块处理不好,要么丢数据,要么无限重试拖垮系统。这篇把状态持久化、重试、超时、休眠四个机制讲透,配套 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 终止重试,避免无谓消耗。休眠让工作流能跑几小时几天,成本几乎为零。下一篇讲条件分支和并行执行,让工作流处理更复杂的业务编排。

上一篇 工作流编排基础
下一篇 分支与并行执行

上一篇
工作流编排基础
下一篇
分支与并行执行