czlonkowski/n8n-skills/n8n-error-handling

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Wire n8n error handling so failures are loud, structured, and recoverable. Use when building any webhook/API workflow, a scheduled or unattended workflow, or any path where a silent failure would drop user-visible work — and whenever the user mentions error handling, onError, continueErrorOutput, error branches/outputs, retries, retryOnFail, Respond to Webhook status codes, 4xx/5xx, Error Trigger, or "my workflow fails silently". Covers per-node error outputs and wiring, retry/self-healing, error-trigger workflows, and 4xx/5xx response shapes.

Install

npx vskill@latest install czlonkowski/n8n-skills/n8n-error-handling

SKILL.md

n8n Error Handling

By default, when an n8n node throws, the whole workflow halts. For an interactive run you're watching, that's fine — you see the red node and fix it. For anything unattended (a webhook API, a cron job, a queue worker, an agent tool), it's the wrong default: the caller gets a timeout or an empty 500, the operator gets no alert, and the symptom is "the integration just stopped working" with no log and no clue.

This skill is about making failures loud, structured, and recoverable — and, best case, self-healing so transient blips never reach a human at all.

The two ideas that prevent most silent failures:

  • Per-node error outputs — a node's failure routes down a second output you control, instead of killing the run.
  • A workflow-level error workflow — a catch-all that fires for anything that escapes per-node handling (timeouts, crashes between nodes, unwired failures).

When you actually need this

Workflow shapeError handling posture
Webhook / API (anything with Respond to Webhook)Required. Every fallible node's error output wired; status code matches cause.
Scheduled / cron / queue worker / agent tool (unattended)Required. A workflow-level error workflow, plus retryOnFail on network nodes.
Internal one-off you run and watch yourselfOptional. Default onError: "stopWorkflow" is fine — you'll see the red node and re-run.

The dividing line: if anyone other than you sees the output — a downstream system, an end user, an on-call engineer — the failure has to be handled, not swallowed. If you're the only watcher and the cost of failure is "I notice and re-run", looser is fine.


The #1 silent trap: per-node error output is a TWO-step setup

This is the single most common way an n8n workflow "handles" errors while actually swallowing them. Routing a node's failure to a handler takes two changes, and doing only one looks complete but misbehaves:

  1. Set `onError: "continueErrorOutput"` on the node. This is what creates the second output. Without it, main[1] doesn't exist no matter what you wire.
  2. Wire that error output (connections.<node>.main[1], i.e. sourceIndex: 1) to a real handler. Without a target, the error data is emitted into the void.

Get one without the other and you hit a failure mode:

What you didWhat happens at runtime
onError set, error output not wiredError data is silently discarded. Downstream doesn't fire. The dashboard shows the run as succeeded. Worst case — no error logged anywhere.
Error output wired, onError not setThe slot never fires; the handler is unreachable. On failure the workflow just halts (default stopWorkflow).
Both doneFailure routes down main[1] to your handler. ✅
Doing both with n8n_update_partial_workflow
javascript
// 1) Turn on the error output (creates main[1])
{ type: "updateNode", nodeName: "HTTP Request",
changes: { onError: "continueErrorOutput" } }
// 2) Wire the error output to a handler. sourceIndex: 1 = the error output.
{ type: "addConnection",
source: "HTTP Request",
target: "Handle Error",
sourceIndex: 1 }

sourceIndex: 0 is the success path, sourceIndex: 1 is the error path. (For IF nodes the aliases branch: "true"/"false" map to index 0/1; for a generic fallible node, use the explicit sourceIndex: 1.)

Then verify. This trap doesn't surface in validate_workflow — a half-wired error output validates clean. Pull the workflow with n8n_get_workflow and confirm both halves:

  • The node's onError is "continueErrorOutput".
  • connections["HTTP Request"].main[1] contains your handler.

Valid onError values:

ValueEffect
"stopWorkflow" (default)Error halts the whole workflow.
"continueRegularOutput"Error item flows out the normal output. Rare, usually wrong — downstream gets error-shaped data and keeps going.
"continueErrorOutput"Error item flows out the separate error output (main[1]). The one you wire.

Full failure-mode catalog, fan-in/fan-out shapes, and verification: NODE_ERROR_OUTPUTS.md.

Failures the error output never sees

A correctly wired error output still misses whole classes of failure. Verified on n8n 2.38.5:

FailureWith continueErrorOutputWith continueRegularOutput
JS error inside `{{ }}` (TypeError on a missing path, JSON.parse on bad input, a thrown Error, a JMESPath syntax error)nothing fires; the field is null and items take the success pathsame
Python Code node rejected before running (blocked import, dunder access: Security violations detected) or bad return shape (list in each-item mode)node marked failed, but the unchanged input items leave through the success output; main[1] stays empty and the execution shows successunchanged input items pass through
Exception while code runs (raise/throw, KeyError, NameError)routed to main[1] as { error } ✅{ error } item on the main output

So an error branch alone can't guard these:

  • Guard the data, not just the node. After a transform whose output matters, check that the

field you produced exists (IF/Filter on {{ $json.total !== undefined && $json.total !== null }}) and send the miss down the error path yourself.

  • Test-run and read node statuses and output values. The Python cases show a red node inside a

green execution. The expression cases show nothing but nulls.

  • Root causes and fixes: n8n-expression-syntax (Debugging) and n8n-code-python (Errors and onError).

Self-healing first: retryOnFail before you wire error paths

Before you build error branches, absorb the transient failures so they never reach those branches. On any node that calls a network service — HTTP Request, comms (Gmail/Slack/Discord), databases, AI nodes, third-party integrations — set node-level retry:

javascript
{ type: "updateNode", nodeName: "HTTP Request",
changes: {
retryOnFail: true,
maxTries: 3,
waitBetweenTries: 5000 // ms
} }

Why this comes first: a 429 or a brief upstream hiccup will retry and usually succeed on its own. The error output then fires only on real, persistent failures — so your 5xx responses and on-call alerts reflect actual problems instead of noise.

Engine limits to know: retry fires on any error (there's no per-status-code filter), maxTries caps at 5, and waitBetweenTries caps at 5000ms — so 5000 is both the max and a sensible default. See n8n-node-configuration (NODE_FAMILY_GOTCHAS.md) for node-specific notes.


API workflows: the canonical shape

A webhook-triggered workflow that responds to its caller has one rule that overrides everything else: no hanging branches. Every path — success and every error — must end at a Respond to Webhook, or the caller sits there until it times out.

Webhook (responseMode: "responseNode")
├── validate input → process → Respond (200, body)
└── (any fallible node's error output → sourceIndex 1)
→ Respond (4xx/5xx, structured error body)
→ optional: log full error privately / notify

Three things make this work:

  1. Fan-in to one error responder. Many fallible nodes can route their main[1] to a single Respond node. Keeps the graph readable.
  2. Validation failures (4xx) are checked *upstream*, not via error outputs. A missing field isn't a node crashing — it's an expected outcome with a known response. Branch on it with IF/Switch (or the per-field validator below) and return 400/401/403/404 directly. Error outputs are for unexpected failures (5xx).
  3. `options.responseCode` defaults to 200 — even on error branches. This is its own silent trap (see RESPONSE_SHAPES.md and n8n-node-configuration NODE_FAMILY_GOTCHAS.md): an error branch that returns 200 with an error body looks like success to the caller's HTTP client, so their error handling never fires. Set responseCode explicitly on every Respond node.
Input validation: conditions on the canvas

Put the checks in one IF node: one boolean condition per rule (is true), combinator and. True → your logic (200); false → a 400 Respond with a static validation_error body that echoes the expected schema, so the caller can self-correct. When the caller needs per-field messages (an LLM client, a public API), replace the IF with one Code node that loops over a schema list and returns { valid, validationError, details, requiredSchema }, with the note Why Code: IF considered — per-field messages need a loop over the schema. Both patterns, the constraint cookbook and the escaping rules are in API_WORKFLOWS.md.


Response shapes: map cause → status code

A 5xx with text/plain "Internal Server Error" is technically an error response and practically useless. And not every failure is a 5xx. Match the status code to *why* the request failed, because the caller branches on it: their monitoring alerts on 5xx (your fault) but not 4xx (their fault), and 5xx suggests "retry" while 4xx suggests "don't".

The common mistake: wiring everything — including bad input — to one Respond that returns 500 internal_error. Now the caller can't tell their bug from your outage, and your error rates can't separate real incidents from client noise.

CauseStatuserror codeWhere it's handled
Required field missing / wrong type400validation_errorUpstream check (IF conditions / per-field validator), not error output
Auth missing or invalid401unauthorizedUpstream check
Authenticated but not allowed403forbiddenUpstream check
Resource ID valid in request, absent in your data404not_foundBranch on the lookup result, not its error
Conflicts with current state (duplicate, race)409conflictDetect with logic
Caller exceeded rate limit429rate_limit_exceededSet Retry-After header
Node threw, cause unknown500internal_errorError output path
Third-party API returned an error502upstream_errorError output of the HTTP node
Can't process right now (downstream down)503service_unavailableDetect specific error, hint retry
Third-party API timed out504upstream_timeoutError output filtered by message

So there are two distinct flows: 4xx is decided before the work (IF/Switch + dedicated Respond), 5xx comes out of error outputs ("we tried, it broke").

Route by cause with a Switch; one static Respond per status. Picking a status code from the error is a routing job. A Switch on the error message classifies it, and each output goes to a Respond with a fixed options.responseCode and a fixed body:

error outputs (main[1]) ─→ Switch "Classify error" (on $json.error.message)
├─ contains "INVALID_ID" → Respond 400 validation_error
├─ contains "429" → Respond 429 rate_limit_exceeded
├─ contains "timeout" → Respond 504 upstream_timeout
└─ fallback (extra output) → Respond 500 internal_error

Every status is visible on the canvas, and adding one is adding a rule. Set the Switch's options.fallbackOutput to extra, or an unmatched error is dropped and the caller hangs. A single Respond with a computed code is fine for exactly two outcomes, as one short ternary in its Response Code field. The Switch JSON is in API_WORKFLOWS.md.

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
5,600
Repository forks
957
7-day trend score
43

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/czlonkowski/n8n-skills/n8n-error-handling/badge)](https://verified-skill.com/skills/czlonkowski/n8n-skills/n8n-error-handling)

Something wrong with this skill or its page? Tell us.