Robust error handling separates production-ready workflows from fragile automations. This guide covers comprehensive error handling patterns for building resilient n8n workflows.
Error Handling Fundamentals
Error Workflow Configuration
// Function Node: Central Error Handler
// This node receives errors from the Error Trigger node
const error = $input.first().json;
// Extract error details
const errorInfo = {
timestamp: new Date().toISOString(),
workflowId: error.workflow?.id,
workflowName: error.workflow?.name,
executionId: error.execution?.id,
nodeName: error.node?.name,
nodeType: error.node?.type,
message: error.message,
stack: error.stack?.substring(0, 1000),
// Categorize error
category: categorizeError(error),
// Determine severity
severity: determineSeverity(error),
// Determine if retryable
retryable: isRetryable(error)
};
function categorizeError(err) {
const message = (err.message || '').toLowerCase();
if (message.includes('rate limit') || message.includes('429')) {
return 'rate_limit';
}
if (message.includes('timeout') || message.includes('ETIMEDOUT')) {
return 'timeout';
}
if (message.includes('auth') || message.includes('401') || message.includes('403')) {
return 'authentication';
}
if (message.includes('not found') || message.includes('404')) {
return 'not_found';
}
if (message.includes('connection') || message.includes('ECONNREFUSED')) {
return 'connection';
}
if (message.includes('validation') || message.includes('invalid')) {
return 'validation';
}
return 'unknown';
}
function determineSeverity(err) {
const category = categorizeError(err);
const nodeName = err.node?.name || '';
// Critical workflows
const criticalNodes = ['payment', 'order', 'billing'];
if (criticalNodes.some(c => nodeName.toLowerCase().includes(c))) {
return 'critical';
}
// Severity by category
const severityMap = {
'authentication': 'high',
'rate_limit': 'medium',
'timeout': 'medium',
'connection': 'medium',
'not_found': 'low',
'validation': 'low',
'unknown': 'medium'
};
return severityMap[category] || 'medium';
}
function isRetryable(err) {
const category = categorizeError(err);
const retryableCategories = ['rate_limit', 'timeout', 'connection'];
return retryableCategories.includes(category);
}
return [{ json: errorInfo }];Error Classification Router
// Switch Node Configuration: Route by Error Category
// Configure branches based on error.category
// Branch 1: Rate Limit Errors
{
"conditions": {
"string": [
{
"value1": "={{ $json.category }}",
"operation": "equals",
"value2": "rate_limit"
}
]
}
}
// Branch 2: Authentication Errors
{
"conditions": {
"string": [
{
"value1": "={{ $json.category }}",
"operation": "equals",
"value2": "authentication"
}
]
}
}
// Branch 3: Timeout/Connection Errors
{
"conditions": {
"string": [
{
"value1": "={{ $json.category }}",
"operation": "in",
"value2": "timeout,connection"
}
]
}
}
// Branch 4: Validation Errors
{
"conditions": {
"string": [
{
"value1": "={{ $json.category }}",
"operation": "equals",
"value2": "validation"
}
]
}
}
// Default: Unknown ErrorsRetry Strategies
Exponential Backoff Implementation
// Function Node: Calculate Retry Delay
const error = $input.first().json;
const retryConfig = {
maxRetries: 5,
baseDelay: 1000, // 1 second
maxDelay: 60000, // 1 minute
factor: 2, // Exponential factor
jitter: true // Add randomness
};
// Get current retry count
const currentRetry = error.retryCount || 0;
if (currentRetry >= retryConfig.maxRetries) {
return [{
json: {
...error,
shouldRetry: false,
reason: `Max retries (${retryConfig.maxRetries}) exceeded`,
action: 'dead_letter'
}
}];
}
// Calculate delay with exponential backoff
let delay = Math.min(
retryConfig.baseDelay * Math.pow(retryConfig.factor, currentRetry),
retryConfig.maxDelay
);
// Add jitter (±25%)
if (retryConfig.jitter) {
const jitterFactor = 0.75 + Math.random() * 0.5;
delay = Math.floor(delay * jitterFactor);
}
// Special handling for rate limits
if (error.category === 'rate_limit') {
// Use Retry-After header if available
const retryAfter = error.headers?.['retry-after'];
if (retryAfter) {
delay = parseInt(retryAfter) * 1000;
} else {
// Default to longer delay for rate limits
delay = Math.max(delay, 30000);
}
}
return [{
json: {
...error,
shouldRetry: true,
retryCount: currentRetry + 1,
delayMs: delay,
nextRetryAt: new Date(Date.now() + delay).toISOString()
}
}];Retry with State Persistence
// Function Node: Persist Retry State to Database
const retryInfo = $input.first().json;
// Build database record
const retryRecord = {
execution_id: retryInfo.executionId,
workflow_id: retryInfo.workflowId,
original_input: JSON.stringify(retryInfo.originalInput),
error_message: retryInfo.message,
retry_count: retryInfo.retryCount,
next_retry_at: retryInfo.nextRetryAt,
status: 'pending_retry',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
return [{
json: {
table: 'workflow_retries',
operation: 'upsert',
data: retryRecord,
conflictField: 'execution_id'
}
}];Circuit Breaker Pattern
Circuit Breaker Implementation
// Function Node: Circuit Breaker Logic
const request = $input.first().json;
const serviceName = request.serviceName || 'default';
// Circuit breaker configuration
const config = {
failureThreshold: 5,
successThreshold: 3,
timeout: 30000, // 30 seconds in open state
halfOpenRequests: 1
};
// Get circuit state from workflow static data
const circuitStates = $getWorkflowStaticData('global');
const circuitKey = `circuit_${serviceName}`;
let circuit = circuitStates[circuitKey] || {
state: 'closed', // closed, open, half-open
failures: 0,
successes: 0,
lastFailure: null,
openedAt: null
};
// Check if circuit should transition
if (circuit.state === 'open') {
const timeSinceOpen = Date.now() - circuit.openedAt;
if (timeSinceOpen >= config.timeout) {
// Transition to half-open
circuit.state = 'half-open';
circuit.successes = 0;
} else {
// Still open, reject request
return [{
json: {
allowed: false,
reason: 'Circuit breaker is open',
retryAfterMs: config.timeout - timeSinceOpen,
circuit: {
state: circuit.state,
failures: circuit.failures
}
}
}];
}
}
// Request allowed
circuitStates[circuitKey] = circuit;
return [{
json: {
allowed: true,
circuit: circuit,
onSuccess: function() {
if (circuit.state === 'half-open') {
circuit.successes++;
if (circuit.successes >= config.successThreshold) {
// Close the circuit
circuit.state = 'closed';
circuit.failures = 0;
}
} else {
circuit.failures = 0;
}
}.toString(),
onFailure: function() {
circuit.failures++;
circuit.lastFailure = Date.now();
if (circuit.failures >= config.failureThreshold) {
// Open the circuit
circuit.state = 'open';
circuit.openedAt = Date.now();
}
}.toString()
}
}];Circuit State Manager
// Function Node: Update Circuit State After Request
const result = $input.first().json;
const serviceName = result.serviceName;
const circuitStates = $getWorkflowStaticData('global');
const circuitKey = `circuit_${serviceName}`;
let circuit = circuitStates[circuitKey];
if (!circuit) {
return [{ json: { updated: false, reason: 'No circuit found' } }];
}
const config = {
failureThreshold: 5,
successThreshold: 3
};
if (result.success) {
// Handle success
if (circuit.state === 'half-open') {
circuit.successes = (circuit.successes || 0) + 1;
if (circuit.successes >= config.successThreshold) {
circuit.state = 'closed';
circuit.failures = 0;
circuit.successes = 0;
}
} else {
circuit.failures = 0;
}
} else {
// Handle failure
circuit.failures = (circuit.failures || 0) + 1;
circuit.lastFailure = Date.now();
if (circuit.failures >= config.failureThreshold && circuit.state !== 'open') {
circuit.state = 'open';
circuit.openedAt = Date.now();
}
}
circuitStates[circuitKey] = circuit;
return [{
json: {
updated: true,
circuit: {
state: circuit.state,
failures: circuit.failures,
successes: circuit.successes
}
}
}];Dead Letter Queue
DLQ Handler
// Function Node: Dead Letter Queue Manager
const failedItem = $input.first().json;
// Build DLQ entry
const dlqEntry = {
id: `dlq_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
timestamp: new Date().toISOString(),
// Original execution info
workflowId: failedItem.workflowId,
workflowName: failedItem.workflowName,
executionId: failedItem.executionId,
// Error details
error: {
message: failedItem.message,
category: failedItem.category,
nodeName: failedItem.nodeName
},
// Original payload
originalPayload: failedItem.originalInput,
// Retry history
retryHistory: failedItem.retryHistory || [],
totalRetries: failedItem.retryCount || 0,
// Status
status: 'unprocessed',
priority: determinePriority(failedItem),
// Metadata for replay
replayable: true,
replayWorkflowId: failedItem.workflowId
};
function determinePriority(item) {
if (item.severity === 'critical') return 1;
if (item.severity === 'high') return 2;
if (item.severity === 'medium') return 3;
return 4;
}
return [{ json: dlqEntry }];DLQ Processor Workflow
// Function Node: Process Dead Letter Queue Items
// This runs on a schedule to retry DLQ items
const dlqItems = $input.all().map(i => i.json);
const now = new Date();
// Configuration
const config = {
maxAgeHours: 72, // Items older than this are archived
retryableCategories: ['timeout', 'connection', 'rate_limit'],
batchSize: 10
};
const toProcess = [];
const toArchive = [];
for (const item of dlqItems) {
const ageHours = (now - new Date(item.timestamp)) / (1000 * 60 * 60);
if (ageHours > config.maxAgeHours) {
toArchive.push({
...item,
status: 'archived',
archivedAt: now.toISOString(),
archiveReason: 'max_age_exceeded'
});
continue;
}
// Check if retryable
if (config.retryableCategories.includes(item.error.category)) {
toProcess.push(item);
} else {
// Non-retryable - needs manual intervention
if (item.status === 'unprocessed') {
toArchive.push({
...item,
status: 'requires_manual_review',
reviewReason: `Non-retryable error category: ${item.error.category}`
});
}
}
}
// Limit batch size
const batch = toProcess.slice(0, config.batchSize);
return [
...batch.map(item => ({
json: {
action: 'retry',
item
}
})),
...toArchive.map(item => ({
json: {
action: 'archive',
item
}
}))
];Alerting and Notifications
Alert Manager
// Function Node: Intelligent Alert Manager
const error = $input.first().json;
// Alert configuration
const alertConfig = {
channels: {
slack: {
enabled: true,
webhook: $env.SLACK_WEBHOOK_URL,
minSeverity: 'medium'
},
email: {
enabled: true,
recipients: ['ops@example.com'],
minSeverity: 'high'
},
pagerduty: {
enabled: true,
serviceKey: $env.PAGERDUTY_SERVICE_KEY,
minSeverity: 'critical'
}
},
// Rate limiting
rateLimits: {
perWorkflow: {
maxAlerts: 5,
windowMinutes: 15
},
perError: {
maxAlerts: 1,
windowMinutes: 60
}
}
};
// Get recent alerts from static data
const alertHistory = $getWorkflowStaticData('global').alertHistory || [];
const now = Date.now();
// Clean old alerts
const recentAlerts = alertHistory.filter(a =>
now - a.timestamp < 60 * 60 * 1000 // Last hour
);
// Check rate limits
function shouldAlert(error, alertType) {
const workflowAlerts = recentAlerts.filter(a =>
a.workflowId === error.workflowId &&
now - a.timestamp < alertConfig.rateLimits.perWorkflow.windowMinutes * 60 * 1000
);
if (workflowAlerts.length >= alertConfig.rateLimits.perWorkflow.maxAlerts) {
return false;
}
const errorAlerts = recentAlerts.filter(a =>
a.errorHash === hashError(error) &&
now - a.timestamp < alertConfig.rateLimits.perError.windowMinutes * 60 * 1000
);
if (errorAlerts.length >= alertConfig.rateLimits.perError.maxAlerts) {
return false;
}
return true;
}
function hashError(err) {
// Create a hash of error for deduplication
return `${err.workflowId}_${err.nodeName}_${err.category}`;
}
// Determine which channels to alert
const severityOrder = ['low', 'medium', 'high', 'critical'];
const errorSeverityIndex = severityOrder.indexOf(error.severity);
const alerts = [];
for (const [channel, config] of Object.entries(alertConfig.channels)) {
if (!config.enabled) continue;
const minSeverityIndex = severityOrder.indexOf(config.minSeverity);
if (errorSeverityIndex >= minSeverityIndex && shouldAlert(error, channel)) {
alerts.push({
channel,
config,
error
});
// Record alert
recentAlerts.push({
timestamp: now,
workflowId: error.workflowId,
errorHash: hashError(error),
channel
});
}
}
// Save alert history
$getWorkflowStaticData('global').alertHistory = recentAlerts;
return alerts.map(a => ({ json: a }));Slack Alert Formatter
// Function Node: Format Slack Alert
const alertData = $input.first().json;
const error = alertData.error;
const severityEmoji = {
critical: '🔴',
high: '🟠',
medium: '🟡',
low: '🟢'
}[error.severity] || '⚪';
const slackPayload = {
text: `${severityEmoji} Workflow Error: ${error.workflowName}`,
blocks: [
{
type: 'header',
text: {
type: 'plain_text',
text: `${severityEmoji} Workflow Error Alert`
}
},
{
type: 'section',
fields: [
{
type: 'mrkdwn',
text: `*Workflow:*\n${error.workflowName}`
},
{
type: 'mrkdwn',
text: `*Severity:*\n${error.severity.toUpperCase()}`
},
{
type: 'mrkdwn',
text: `*Failed Node:*\n${error.nodeName}`
},
{
type: 'mrkdwn',
text: `*Category:*\n${error.category}`
}
]
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Error Message:*\n\`\`\`${error.message.substring(0, 500)}\`\`\``
}
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `Execution ID: ${error.executionId} | Time: ${error.timestamp}`
}
]
},
{
type: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: 'View Execution'
},
url: `${$env.N8N_URL}/execution/${error.executionId}`
},
{
type: 'button',
text: {
type: 'plain_text',
text: 'View Workflow'
},
url: `${$env.N8N_URL}/workflow/${error.workflowId}`
}
]
}
]
};
return [{ json: slackPayload }];Error Recovery Patterns
Compensating Transaction
// Function Node: Compensating Transaction Handler
const failedOperation = $input.first().json;
// Track completed steps for rollback
const completedSteps = failedOperation.completedSteps || [];
// Define compensation actions for each step
const compensationActions = {
'create_order': async (stepData) => ({
action: 'cancel_order',
orderId: stepData.orderId
}),
'reserve_inventory': async (stepData) => ({
action: 'release_inventory',
items: stepData.reservedItems
}),
'charge_payment': async (stepData) => ({
action: 'refund_payment',
transactionId: stepData.transactionId,
amount: stepData.amount
}),
'send_notification': async (stepData) => ({
action: 'send_cancellation_notice',
recipient: stepData.recipient
})
};
// Build compensation plan (reverse order)
const compensationPlan = completedSteps
.reverse()
.map(step => ({
originalStep: step.name,
compensationAction: compensationActions[step.name],
stepData: step.data,
status: 'pending'
}))
.filter(step => step.compensationAction); // Only steps with defined compensation
return [{
json: {
failedAt: failedOperation.failedStep,
compensationPlan,
originalExecutionId: failedOperation.executionId
}
}];Saga Pattern Implementation
// Function Node: Saga Coordinator
const sagaState = $input.first().json;
// Saga step definitions
const sagaSteps = [
{
name: 'validate_request',
action: 'validateOrder',
compensation: null // No compensation needed
},
{
name: 'reserve_inventory',
action: 'reserveItems',
compensation: 'releaseItems'
},
{
name: 'process_payment',
action: 'chargeCustomer',
compensation: 'refundCustomer'
},
{
name: 'fulfill_order',
action: 'createShipment',
compensation: 'cancelShipment'
},
{
name: 'send_confirmation',
action: 'sendEmail',
compensation: 'sendCancellation'
}
];
// Initialize or get saga state
const saga = sagaState.saga || {
id: `saga_${Date.now()}`,
status: 'running',
currentStep: 0,
completedSteps: [],
compensating: false,
input: sagaState.input
};
// Determine next action
let nextAction;
if (saga.compensating) {
// Executing compensation
const stepToCompensate = saga.completedSteps.pop();
if (stepToCompensate) {
const stepDef = sagaSteps.find(s => s.name === stepToCompensate.name);
nextAction = {
type: 'compensate',
action: stepDef.compensation,
data: stepToCompensate.result
};
} else {
// All compensations complete
saga.status = 'compensated';
nextAction = {
type: 'complete',
status: 'rolled_back'
};
}
} else {
// Normal execution
if (saga.currentStep < sagaSteps.length) {
const currentStepDef = sagaSteps[saga.currentStep];
nextAction = {
type: 'execute',
step: currentStepDef.name,
action: currentStepDef.action,
data: saga.input
};
} else {
// All steps complete
saga.status = 'completed';
nextAction = {
type: 'complete',
status: 'success'
};
}
}
return [{
json: {
saga,
nextAction
}
}];Best Practices Summary
- Categorize errors for appropriate handling strategies
- Use exponential backoff with jitter for retries
- Implement circuit breakers for external services
- Maintain dead letter queues for failed items
- Rate limit alerts to prevent notification fatigue
- Design compensating transactions for multi-step processes
- Log everything for debugging and analysis
- Test failure scenarios to validate error handling
Robust error handling transforms fragile workflows into resilient, production-ready automations. Invest in error handling early to prevent issues at scale.