Skip to content

The message-based NonRetryableError escape hatch also fails to settle the instance (sibling of #6) #7

Description

@matt-jamie-milne

Summary

Sibling of #6. That issue reports NonRetryableError losing its class across the runner stub, so the engine retries. This one reports that the engine's message-based escape hatch — which does survive the stub — does not make the instance settle either.

The engine's own check is name-or-message, not instanceof (miniflare dist/src/workers/workflows/binding.worker.js):

err.name === 'NonRetryableError' || err.message.startsWith('NonRetryableError')

Since #6 establishes that the name is dropped crossing the stub while the message survives, prefixing the message looks like a valid workaround from tenant code that cannot import cloudflare:workflows. It isn't.

Version: @cloudflare/dynamic-workflows@0.1.1, wrangler@4.120.1, local wrangler dev.

Repro

Same dispatcher wiring as #6createDynamicWorkflowEntrypoint, DynamicWorkflowBinding re-exported, instances via wrapWorkflowBinding(metadata), tenant module through env.LOADER.get(id, …) with globalOutbound: null. The tenant throws inside step.do, with both signals set and without importing cloudflare:workflows:

export default {
  async run(gateway, event, step) {
    return step.do('denied_step', async () => {
      const fatal = new Error('NonRetryableError: policy_violation')
      fatal.name = 'NonRetryableError'
      throw fatal
    })
  }
}

Expected

The step fails immediately; the instance reaches errored once.

Actual

The instance does not reach a terminal status. Polling instance.status() every 250ms for 10s never observes errored, complete or terminated, and the sentinel message appears several times in that window, consistent with the step still being retried.

One observation I could not fully resolve: the message comes back with the NonRetryableError: prefix stripped, which in the miniflare source only happens when PreservedNonRetryableError is constructed. That means a fatal branch is reached — but I could not determine whether it is the step-level branch or the outer one after the retry budget is exhausted, so I am not claiming the step-level check matched.

Why it matters

#6's workaround-shaped reading is "set the message prefix instead of relying on the class". If that also fails, tenant code inside a Dynamic Worker has no way at all to mark a failure terminal, and every deterministic failure — a validation error from a downstream API, for instance — costs the full retry budget. In our case a rejected write took six attempts across roughly thirty seconds to fail in a way that was decided in sixty milliseconds.

Happy to test a patch or provide a fuller harness.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions