data/skills/n8n-error-handling/NODE_ERROR_OUTPUTS.md
This file is about the error output on a single node — the second main output that fires when that node throws — and the two-step setup that trips up nearly everyone. For the workflow-level catch-all (Error Trigger workflows) and the webhook/Respond shape, see the rest of n8n-error-handling.
The whole point: a node failing should route somewhere you control, instead of halting the run. The cost of forgetting half the setup is one of the worst silent-failure modes in n8n — a run that shows green while quietly dropping its work.
Routing a node's failure takes exactly two changes. Either one alone looks finished and misbehaves.
Set onError: "continueErrorOutput" on the node. This is what adds the second output. Until you do, main[1] does not exist and nothing you wire to it can fire.
{ type: "updateNode", nodeName: "Google Sheets",
changes: { onError: "continueErrorOutput" } }
Surgical alternative if you're touching only this field:
{ type: "patchNodeField", nodeName: "Google Sheets",
fieldPath: "onError", value: "continueErrorOutput" }
The valid onError values:
| Value | Effect |
|---|---|
"stopWorkflow" (default) | Error halts the whole workflow. The right default for runs you watch. |
"continueRegularOutput" | The error item flows out the normal output (main[0]) alongside successes. Rare and usually a mistake — downstream gets error-shaped data and keeps going. |
"continueErrorOutput" | The error item flows out a separate error output (main[1]). This is the one you wire below. |
With onError: "continueErrorOutput", the node has two outputs:
main[0] → success path (sourceIndex: 0)main[1] → error path (sourceIndex: 1)Wire the error output to a real handler:
{ type: "addConnection",
source: "Google Sheets",
target: "Handle Error",
sourceIndex: 1 }
sourceIndex: 1 is the error output. (IF nodes accept the friendly aliases branch: "true"/branch: "false" for index 0/1; a generic fallible node has no such alias — use the explicit sourceIndex: 1.)
onError set, error output NOT wired// onError: "continueErrorOutput" set on the node,
// but no addConnection from sourceIndex 1.
On failure the node emits to main[1], which has no targets. The error data is silently discarded, downstream never fires, and — this is the trap — the execution is recorded as succeeded, because from n8n's perspective the error was "handled" by a branch that happens to go nowhere. No failed execution logged, nothing in the dashboard. The integration "just stops working" and there's no trail.
Fix: wire sourceIndex: 1 to a real handler, or set onError back to "stopWorkflow" so the failure is loud again.
onError NOT set// addConnection from "Some Node" sourceIndex 1 → "Handle Error" exists,
// but the node still has the default onError: "stopWorkflow".
The connection sits in the JSON, but the slot it feeds from never fires. The handler is unreachable. On failure the workflow simply halts (default behavior). Less dangerous than the first mode — at least it's loud — but the handler you built does nothing.
Fix: set onError: "continueErrorOutput" on the node.
A half-wired error output validates clean. validate_workflow and n8n_validate_workflow don't flag "onError is set but main[1] is empty" or vice versa — both are structurally legal. This is a runtime behavior, not a schema violation. The only reliable check is to read the workflow back (see Verification below).
// Node config: onError: "continueErrorOutput"
{ type: "addConnection", source: "HTTP Request", target: "Respond Error", sourceIndex: 1 }
{ type: "addConnection", source: "HTTP Request", target: "Save Result", sourceIndex: 0 }
{ type: "addConnection", source: "HTTP Request", target: "Notify Slack", sourceIndex: 0 }
{ type: "addConnection", source: "HTTP Request", target: "Respond Error", sourceIndex: 1 }
// Each of these nodes needs onError: "continueErrorOutput" on its own config.
{ type: "addConnection", source: "Fetch User", target: "Respond Error", sourceIndex: 1 }
{ type: "addConnection", source: "Call External", target: "Respond Error", sourceIndex: 1 }
{ type: "addConnection", source: "Write Database", target: "Respond Error", sourceIndex: 1 }
Fan-in keeps the graph readable: one error responder, many sources. The handler can inspect which node failed (the error payload carries the failing node's name) to differentiate the response.
Wiring the error output to two targets composes without conflict — both receive the error data:
{ type: "addConnection", source: "Call External", target: "Log Full Error", sourceIndex: 1 }
{ type: "addConnection", source: "Call External", target: "Respond Error", sourceIndex: 1 }
Useful when you want a sanitized response and a private full-detail log on the same failure. (Or chain them: error output → Log → Respond, so the log runs first.)
Wire an error output on anything that can throw at runtime:
retryOnFail first so these self-heal).Usually not worth an error output:
When unsure, wire it. The cost is one connection; the cost of not wiring it is a silent halt.
After any create/update, pull the workflow with n8n_get_workflow and check both halves on each fallible node:
onError is "continueErrorOutput" (or whatever you intended).connections["<node>"].main[1] contains the expected handler(s).If either half is missing, you have a silent-failure setup. Fix before activating.
n8n_autofix_workflow can repair some structural issues, but it won't infer that you meant to wire an error path — the intent to handle a given node's failure is yours to express. Treat the read-back as mandatory.
Per-node outputs handle the failure of one node you remembered to wire. They do not catch:
For those, you need a workflow-level error workflow (Error Trigger node). And note the inverse: a per-node error output that's wired to a no-op which drops the data counts as "handled" — so it will suppress the error workflow. Only catch per-node when you're genuinely acting on the error. See ERROR_WORKFLOWS.md.