n8n Advanced: Error Handling, Sub-Workflows, and Code Nodes Done Right (2026)
The gap between a junior n8n builder and a senior one isn’t about knowing more nodes. It’s about three skills:
- Error handling — Your workflows don’t just work; they fail gracefully
- Sub-workflows — You don’t copy-paste; you build reusable components
- Code nodes — When the drag-and-drop hits a wall, you write your way through
Master these three and you’ll build automations that your past self would think required a full-time engineer.
Part 1: Error Handling That Doesn’t Suck
The default n8n error behavior is brutal: one node fails → entire execution stops → data in flight is lost. For anything running in production, this is unacceptable.
The Error Trigger Node
n8n has a built-in Error Trigger node. When any workflow fails, a separate “error workflow” fires — giving you a chance to log, alert, and retry.
Set it up:
- Create a new workflow called
Error Handler - Add an Error Trigger node as the first step (it has no configuration — it just listens)
- Chain your error handling logic after it
Here’s a production-grade error workflow:
Error Trigger
↓
IF (Check if execution.error contains useful info)
↓ YES
├─ Slack: Send alert to #alerts channel
│ Text: "⚠️ Workflow *{{ $json.workflow.name }}* failed.
│ Error: {{ $json.error.message }}
│ Execution ID: {{ $json.execution.id }}"
├─ Google Sheets: Log error (timestamp, workflow, error message, execution ID)
└─ Wait (5 min, to avoid thrashing)
→ Re-execute Workflow node (retry the failed run once)
↓ NO
└─ Ignore (not a real error)
In-Workflow Error Handling
For individual nodes that might fail (API timeouts, rate limits), don’t rely on the workflow-level Error Trigger. Use error output branches:
- Click any node in the editor
- Go to Settings → On Error
- Choose “Continue (using error output)”
Now the node has TWO outputs: a green “success” branch and a red “error” branch. Handle each separately:
HTTP Request (call external API)
├─ Success → Process data → Next step
└─ Error → Check status code
├─ 429 (rate limited) → Wait 60s → Retry
├─ 404 → Log and skip
└─ Other → Stop and alert
The Retry Pattern That Actually Works
APIs fail. The fix isn’t more error logging — it’s intelligent retry:
// In a Code node after an HTTP Request error branch
const attempts = $input.first().json.attempts || 1;
const maxRetries = 3;
if (attempts < maxRetries) {
// Exponential backoff: 2^attempt seconds
const waitSeconds = Math.pow(2, attempts) * 1000;
return {
retry: true,
waitMs: waitSeconds,
attempt: attempts + 1,
lastError: $input.first().json.error
};
}
return {
retry: false,
reason: `Failed after ${maxRetries} attempts`,
lastError: $input.first().json.error
};
Feed this into a Wait node → Loop back to the HTTP Request.
Part 2: Sub-Workflows — Build Once, Use Everywhere
Sub-workflows are n8n’s version of functions. Instead of copy-pasting the same 5 nodes across 10 workflows, you build it once as a standalone workflow and call it from anywhere.
When to Extract a Sub-Workflow
You should create a sub-workflow when:
- The same logic appears in 3+ places
- The logic is complex enough to be tested independently
- You want a non-technical teammate to use it without understanding the internals
Example: Universal “Slack Notifier” Sub-Workflow
Main workflow (caller):
Some trigger → Process data → Execute Workflow (call the sub-workflow)
Sub-workflow slack-notifier:
Workflow Trigger (receives input)
↓
IF: Check channel is provided
↓ YES → Slack: Send message to {{channel}}
↓ NO → Slack: Send message to #general (default)
↓
Return: { status: "sent", channel: "...", timestamp: "..." }
Now every workflow in your organization can use the same Slack notification logic — with default channel, error handling, and consistent formatting — without duplicating a single node.
Sub-Workflow Best Practices
- Name them clearly.
slack-notifier, notwf-helper-3. You’ll thank yourself in 6 months. - Document inputs and outputs. Add a Sticky Note at the top of the sub-workflow describing what it expects and what it returns.
- Version them. When you change a sub-workflow, it affects every workflow that calls it. Tag versions in the name (
slack-notifier-v2) during breaking changes. - Test independently. Before calling a sub-workflow from 10 places, run it manually with sample input. The Workflow Trigger node lets you test in isolation.
Part 3: Code Nodes — When Drag-and-Drop Isn’t Enough
The Code node is n8n’s escape hatch. It runs JavaScript (Node.js 20+) with access to the full data context. When you hit the limits of drag-and-drop, you write code.
When to Reach for Code
| Situation | Drag-and-Drop Alternative |
|---|---|
| Complex data transformation | IF + Set + Merge (10+ nodes) |
| Date/time math | Multiple Date/Time nodes |
| Conditional logic with many branches | Nested IF nodes (painful) |
| API response shaping | Multiple Set nodes |
| Custom encryption/hashing | Not possible otherwise |
Essential Code Node Patterns
1. Data Transformation
// Input: array of orders from Shopify
// Output: aggregated stats
const orders = $input.all();
const stats = {
totalRevenue: orders.reduce((sum, o) => sum + o.json.total_price, 0),
averageOrder: 0,
byProduct: {},
byCustomer: {}
};
orders.forEach(order => {
const o = order.json;
stats.byCustomer[o.customer_email] =
(stats.byCustomer[o.customer_email] || 0) + o.total_price;
o.line_items.forEach(item => {
stats.byProduct[item.title] =
(stats.byProduct[item.title] || 0) + item.quantity;
});
});
stats.averageOrder = stats.totalRevenue / orders.length;
return [{
json: {
totalRevenue: `$${stats.totalRevenue.toFixed(2)}`,
averageOrder: `$${stats.averageOrder.toFixed(2)}`,
topProduct: Object.entries(stats.byProduct)
.sort((a, b) => b[1] - a[1])[0],
topCustomer: Object.entries(stats.byCustomer)
.sort((a, b) => b[1] - a[1])[0]
}
}];
2. Date/Time Without the Pain
// n8n's built-in date nodes are fine for simple stuff.
// For anything else:
const now = new Date();
const then = new Date($input.first().json.created_at);
const result = {
iso_date: now.toISOString().split('T')[0],
unix_timestamp: Math.floor(now.getTime() / 1000),
days_since_created: Math.floor((now - then) / (1000 * 60 * 60 * 24)),
is_weekend: [0, 6].includes(now.getDay()),
next_business_day: (() => {
const d = new Date(now);
d.setDate(d.getDate() + 1);
while (d.getDay() === 0 || d.getDay() === 6) d.setDate(d.getDate() + 1);
return d.toISOString().split('T')[0];
})(),
quarter: Math.floor(now.getMonth() / 3) + 1,
week_number: (() => {
const start = new Date(now.getFullYear(), 0, 1);
return Math.ceil(((now - start) / 86400000 + start.getDay() + 1) / 7);
})()
};
return [{ json: result }];
3. Batch Processing Large Datasets
// When you have 10,000 records but the API takes max 100 at a time
const allItems = $input.all();
const BATCH_SIZE = 100;
const batches = [];
for (let i = 0; i < allItems.length; i += BATCH_SIZE) {
batches.push({
batch: Math.floor(i / BATCH_SIZE) + 1,
totalBatches: Math.ceil(allItems.length / BATCH_SIZE),
items: allItems.slice(i, i + BATCH_SIZE)
});
}
// Return multiple output items — next node loops over each batch
return batches.map(b => ({ json: b }));
Code Node Gotchas
-
Always return an array of
{ json: ... }objects. This is n8n’s data format. Returning a plain object will break downstream nodes. -
Use
$input.first().jsonfor single items,$input.all()for arrays. Method names are from the Item Lists node — they behave the same way. -
Console.log doesn’t show in the editor. Use
$execution.resumeUrlor write to a Google Sheet for debugging. -
Node.js built-in modules are available.
crypto,zlib,buffer— anything in Node.js 20 standard library works.
Putting It All Together: A Production-Grade Workflow
Here’s a workflow that uses all three techniques — error handling, sub-workflows, and code nodes:
Goal: Process 1,000 Shopify orders, compute daily stats, and notify the team.
Architecture:
Cron Trigger (every night at 23:00)
↓
Shopify: Get Orders (today, limit 250)
↓
Loop Over Items (process in batches of 250)
↓
Code Node: Aggregate stats (Part 3, pattern 1)
↓
Execute Workflow: slack-notifier (Part 2)
├─ Channel: #daily-metrics
└─ Message: Formatted from aggregate stats
↓
Error Trigger (Part 1, separate workflow)
└─ If batch fails → log → retry once → escalate to #alerts if still failing
This handles thousands of orders, survives API failures, notifies the team, and never silently loses data.
The Skill Ladder
| Level | What You Build | Key Skill |
|---|---|---|
| Beginner | Linear workflows with 3-5 nodes | Understanding triggers and actions |
| Intermediate | Branching logic, filters, data mapping | Thinking in data flow |
| Advanced | Error handling, sub-workflows, code nodes | You are here |
| Expert | Custom nodes, self-hosted scaling, CI/CD for workflows | n8n as a platform |
The jump from Intermediate to Advanced is the highest-leverage one. It’s the difference between “I can build a workflow” and “I can build a system.”
Keep Learning
If you haven’t already, read our guides on:
- n8n vs Make: Full Cost Comparison — understand the pricing implications of self-hosting
- Self-Host n8n on a $5 VPS — run your advanced workflows without execution limits
- AI Workflow Automation — add LLMs to your error handling and code nodes
Disclosure: Some links are affiliate links. We may earn a commission if you sign up, at no extra cost to you.