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:

  1. Error handling — Your workflows don’t just work; they fail gracefully
  2. Sub-workflows — You don’t copy-paste; you build reusable components
  3. 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:

  1. Create a new workflow called Error Handler
  2. Add an Error Trigger node as the first step (it has no configuration — it just listens)
  3. 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:

  1. Click any node in the editor
  2. Go to SettingsOn Error
  3. 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

  1. Name them clearly. slack-notifier, not wf-helper-3. You’ll thank yourself in 6 months.
  2. Document inputs and outputs. Add a Sticky Note at the top of the sub-workflow describing what it expects and what it returns.
  3. 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.
  4. 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

SituationDrag-and-Drop Alternative
Complex data transformationIF + Set + Merge (10+ nodes)
Date/time mathMultiple Date/Time nodes
Conditional logic with many branchesNested IF nodes (painful)
API response shapingMultiple Set nodes
Custom encryption/hashingNot 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

  1. Always return an array of { json: ... } objects. This is n8n’s data format. Returning a plain object will break downstream nodes.

  2. Use $input.first().json for single items, $input.all() for arrays. Method names are from the Item Lists node — they behave the same way.

  3. Console.log doesn’t show in the editor. Use $execution.resumeUrl or write to a Google Sheet for debugging.

  4. 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

LevelWhat You BuildKey Skill
BeginnerLinear workflows with 3-5 nodesUnderstanding triggers and actions
IntermediateBranching logic, filters, data mappingThinking in data flow
AdvancedError handling, sub-workflows, code nodesYou are here
ExpertCustom nodes, self-hosted scaling, CI/CD for workflowsn8n 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:


Disclosure: Some links are affiliate links. We may earn a commission if you sign up, at no extra cost to you.