# Per-Node Error Outputs 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. --- ## The two-step setup (both are required) Routing a node's failure takes exactly two changes. Either one alone looks finished and misbehaves. ### Step 1 — create the error output 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. ```javascript { type: "updateNode", nodeName: "Google Sheets", changes: { onError: "continueErrorOutput" } } ``` Surgical alternative if you're touching only this field: ```javascript { 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. | ### Step 2 — wire the error output 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: ```javascript { 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`.) --- ## Failure modes — why "one of two" is so dangerous ### `onError` set, error output NOT wired ```javascript // 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. ### Error output wired, `onError` NOT set ```javascript // 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. ### Why validation won't save you 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). --- ## Common wiring shapes ### Single fallible node → error handler ```javascript // Node config: onError: "continueErrorOutput" { type: "addConnection", source: "HTTP Request", target: "Respond Error", sourceIndex: 1 } ``` ### Success path fans out, error path goes elsewhere ```javascript { 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 } ``` ### Multiple fallible nodes → one shared error handler (fan-in) ```javascript // 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. ### Both log AND respond on the same failure Wiring the error output to two targets composes without conflict — both receive the error data: ```javascript { 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.) --- ## What counts as "fallible" Wire an error output on anything that can throw at runtime: - Network calls — HTTP Request, third-party API nodes, databases. - Auth failures — expired credential, rotated token. - Schema mismatches — missing DB column, JSON parse failure. - Rate limits — 429 from upstream (configure `retryOnFail` first so these self-heal). - File/binary operations — missing path, permission denied (see **n8n-binary-and-data**). - Code nodes that can throw. Usually **not** worth an error output: - Set / Edit Fields on already-validated data. - IF / Switch with simple expressions — if those throw it's a bug to fix, not a path to catch. - Pure transformations with no I/O. When unsure, wire it. The cost is one connection; the cost of not wiring it is a silent halt. --- ## Verification (do this every time) After any create/update, pull the workflow with `n8n_get_workflow` and check **both halves** on each fallible node: 1. **Node config** — `onError` is `"continueErrorOutput"` (or whatever you intended). 2. **Connections** — `connections[""].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. --- ## When to use an error workflow instead Per-node outputs handle the failure of *one node you remembered to wire*. They do **not** catch: - Failures on nodes you forgot to wire. - Crashes between nodes. - Whole-workflow timeouts. - Trigger failures. 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**.