Skip to content

Error handling

Any node that talks to the outside world can fail — an HTTP call times out, an email bounces, a database query is rejected. FlowRunner gives every node a built-in try/catch so you decide, per node, whether a failure is handled or fatal.

The error port

Every working node has a hidden error output port. When the node fails, the engine looks at that port:

  • If you wired the error port to another node, the failure routes down that branch. The run keeps going — the error is caught. This is your "catch" block.
  • If you left the error port unwired, the failure surfaces: the node continues on its normal default path, and the run is marked failed overall.

Wiring the error port is how you turn an unavoidable "this might fail" into a graceful branch — retry, notify, fall back to a default, or return a friendly message.

TIP

Only wire the error port where a failure is genuinely recoverable. For everything else, leaving it unwired is correct — you want the run to fail loudly so it shows up as an error in Run history.

Retrying transient failures

External calls fail for reasons that pass on their own — a dropped connection, a timeout, a 429 Too Many Requests, a brief 503. Rather than wiring a manual retry loop, the I/O nodes (HTTP, MongoDB, Email, Slack) can retry automatically. Two config fields turn it on:

FieldMeaning
retriesHow many times to retry after the first attempt — 0 (the default) means no retry. Capped at 5.
retryDelayMsHow long to wait between attempts, in milliseconds.

A retry fires when the call throws (network error, timeout, SSRF block) and, for HTTP and Slack, when it returns a retryable status — HTTP 408/425/429/500/502/503/504, or a retryable status from a Slack webhook (the same set) — a permanent Slack error like 400/404 is not retried. A 404 or other non-retryable status is not retried; it's returned as normal so you can branch on it. Once the retries are exhausted, the node takes its error port just like any other failure.

WARNING

Retries can double-apply a non-idempotent action. If the call actually succeeded on the far end but the acknowledgement was lost — a POST the server already processed, an insertOne that committed, a Slack message that was delivered — the retry runs it again, placing a second order, inserting a duplicate row, or posting twice. Only enable retries where a repeat is safe: reads (GET, findOne) are always safe; for writes, prefer an endpoint that de-duplicates (an idempotency key, a unique index, an upsert). Retrying a 429 is safe (the request was rejected, not processed); the risk is a 5xx or a dropped connection after the work was done. Leave retries at 0 for payments and one-off side effects.

NOTE

Retry waits and the time each failed attempt takes are charged to the run's ~60-second blocking budget (shared with Delay nodes), and retrying stops once that budget is spent — so retries stay bounded even in a loop. The Email node never double-sends on a lost ack (its send is best-effort and only a genuinely-reported failure is retried); a missing email provider is treated as permanent and not retried.

refs.error

Whenever a node fails, the engine writes a bucket named error into the flow's shared refs:

error = {
  message: "http: fetch failed",   // what went wrong
  nodeId:  "node_a1b2c3"            // which node produced it
}

Your handler branch can read ${error.message} in an email or a Template to report exactly what happened, and error.nodeId to say where. Because it lives in refs, the error is available to every node downstream of the failure.

When a run is reported as failed

A run is reported as failed (HTTP 500, ok: false) only when a node failed and the flow never reached an End node. In other words:

  • A caught failure that routes to a handler and still reaches an End → the run succeeds and returns your handler's response.
  • An uncaught failure that ends the path without reaching End → the run fails with a 500.

Whether that failed response names the failing node depends on who is calling — the same trace-exposure gate that governs ?_trace. An authorized caller — a signed-in project member (as the editor's Test-run inspector is), a caller who passed the flow's Basic/Bearer credentials, or any caller when FLOW_TRACE=1 is set — receives the specific error message and the id of the node that failed. An anonymous caller of a public (auth:none) flow gets only a generic { ok: false, errors: ['flow execution failed'] } 500 with no node id. Either way, Run history still records the failing node id and message server-side, so you can always trace it there.

Guardrails

Two hard limits protect the server from a flow that would otherwise never stop — an accidental cycle in the wiring, or a loop over an enormous list:

GuardrailLimitWhat happens when hit
Execution steps10,000The run aborts with "execution step limit exceeded (possible cycle)".
ForEach iterations10,000The ForEach node fails with a "too many iterations" error.

The step cap counts every hand-off from one node to the next across the whole run, so a tight loop wired back on itself is stopped rather than hanging the request. The iteration cap is checked before a ForEach starts, so an oversized collection fails fast instead of grinding through millions of passes.

NOTE

These caps are safety nets, not design targets. A well-built flow finishes in a handful of steps. If you're anywhere near 10,000, revisit the wiring — you very likely have a cycle.

See also

FlowRunner — the no-code platform for small businesses.