六维教程

状态与错误处理

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

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

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