n8n Automation

Patternuri de Error Handling in n8n: Construieste Workflow-uri Reziliente

Petru Constantin
--11 min lectura
#n8n#error handling#workflow automation#resilience#monitoring

Un error handling robust separa workflow-urile gata de productie de automatizarile fragile. Acest ghid acopera patternuri complete de error handling pentru construirea de workflow-uri n8n reziliente.

Fundamentele Error Handling

Configurarea Error Workflow

// Function Node: Central Error Handler
// Acest nod primeste erori de la Error Trigger node
 
const error = $input.first().json;
 
// Extrage detaliile erorii
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),
 
  // Categorizeaza eroarea
  category: categorizeError(error),
 
  // Determina severitatea
  severity: determineSeverity(error),
 
  // Determina daca se poate face retry
  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 || '';
 
  // Workflow-uri critice
  const criticalNodes = ['payment', 'order', 'billing'];
  if (criticalNodes.some(c => nodeName.toLowerCase().includes(c))) {
    return 'critical';
  }
 
  // Severitate dupa categorie
  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 }];

Router de Clasificare a Erorilor

// Switch Node Configuration: Directioneaza dupa categoria erorii
// Configureaza branch-uri pe baza error.category
 
// Branch 1: Erori Rate Limit
{
  "conditions": {
    "string": [
      {
        "value1": "={{ $json.category }}",
        "operation": "equals",
        "value2": "rate_limit"
      }
    ]
  }
}
 
// Branch 2: Erori de Autentificare
{
  "conditions": {
    "string": [
      {
        "value1": "={{ $json.category }}",
        "operation": "equals",
        "value2": "authentication"
      }
    ]
  }
}
 
// Branch 3: Erori Timeout/Conexiune
{
  "conditions": {
    "string": [
      {
        "value1": "={{ $json.category }}",
        "operation": "in",
        "value2": "timeout,connection"
      }
    ]
  }
}
 
// Branch 4: Erori de Validare
{
  "conditions": {
    "string": [
      {
        "value1": "={{ $json.category }}",
        "operation": "equals",
        "value2": "validation"
      }
    ]
  }
}
 
// Default: Erori Necunoscute

Strategii de Retry

Implementare Exponential Backoff

// Function Node: Calculeaza Delay-ul de Retry
const error = $input.first().json;
 
const retryConfig = {
  maxRetries: 5,
  baseDelay: 1000, // 1 secunda
  maxDelay: 60000, // 1 minut
  factor: 2, // Factor exponential
  jitter: true // Adauga aleatoriu
};
 
// Obtine numarul curent de retry-uri
const currentRetry = error.retryCount || 0;
 
if (currentRetry >= retryConfig.maxRetries) {
  return [{
    json: {
      ...error,
      shouldRetry: false,
      reason: `Numar maxim de retry-uri (${retryConfig.maxRetries}) depasit`,
      action: 'dead_letter'
    }
  }];
}
 
// Calculeaza delay cu exponential backoff
let delay = Math.min(
  retryConfig.baseDelay * Math.pow(retryConfig.factor, currentRetry),
  retryConfig.maxDelay
);
 
// Adauga jitter (±25%)
if (retryConfig.jitter) {
  const jitterFactor = 0.75 + Math.random() * 0.5;
  delay = Math.floor(delay * jitterFactor);
}
 
// Tratare speciala pentru rate limits
if (error.category === 'rate_limit') {
  // Foloseste header-ul Retry-After daca este disponibil
  const retryAfter = error.headers?.['retry-after'];
  if (retryAfter) {
    delay = parseInt(retryAfter) * 1000;
  } else {
    // Delay implicit mai lung pentru 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 cu Persistenta Starii

// Function Node: Persista Starea de Retry in Baza de Date
const retryInfo = $input.first().json;
 
// Construieste inregistrarea din baza de date
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'
  }
}];

Patternul Circuit Breaker

Implementarea Circuit Breaker

// Function Node: Logica Circuit Breaker
const request = $input.first().json;
const serviceName = request.serviceName || 'default';
 
// Configuratia circuit breaker
const config = {
  failureThreshold: 5,
  successThreshold: 3,
  timeout: 30000, // 30 secunde in starea open
  halfOpenRequests: 1
};
 
// Obtine starea circuitului din 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
};
 
// Verifica daca circuitul trebuie sa faca tranzitie
if (circuit.state === 'open') {
  const timeSinceOpen = Date.now() - circuit.openedAt;
 
  if (timeSinceOpen >= config.timeout) {
    // Tranzitie la half-open
    circuit.state = 'half-open';
    circuit.successes = 0;
  } else {
    // Inca deschis, respinge request-ul
    return [{
      json: {
        allowed: false,
        reason: 'Circuit breaker este deschis',
        retryAfterMs: config.timeout - timeSinceOpen,
        circuit: {
          state: circuit.state,
          failures: circuit.failures
        }
      }
    }];
  }
}
 
// Request permis
circuitStates[circuitKey] = circuit;
 
return [{
  json: {
    allowed: true,
    circuit: circuit,
    onSuccess: function() {
      if (circuit.state === 'half-open') {
        circuit.successes++;
        if (circuit.successes >= config.successThreshold) {
          // Inchide circuitul
          circuit.state = 'closed';
          circuit.failures = 0;
        }
      } else {
        circuit.failures = 0;
      }
    }.toString(),
    onFailure: function() {
      circuit.failures++;
      circuit.lastFailure = Date.now();
 
      if (circuit.failures >= config.failureThreshold) {
        // Deschide circuitul
        circuit.state = 'open';
        circuit.openedAt = Date.now();
      }
    }.toString()
  }
}];

Managerul Starii Circuitului

// Function Node: Actualizeaza Starea Circuitului Dupa 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: 'Niciun circuit gasit' } }];
}
 
const config = {
  failureThreshold: 5,
  successThreshold: 3
};
 
if (result.success) {
  // Gestioneaza succesul
  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 {
  // Gestioneaza esecul
  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

Handler DLQ

// Function Node: Manager Dead Letter Queue
const failedItem = $input.first().json;
 
// Construieste intrarea DLQ
const dlqEntry = {
  id: `dlq_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
  timestamp: new Date().toISOString(),
 
  // Informatii executie originala
  workflowId: failedItem.workflowId,
  workflowName: failedItem.workflowName,
  executionId: failedItem.executionId,
 
  // Detalii eroare
  error: {
    message: failedItem.message,
    category: failedItem.category,
    nodeName: failedItem.nodeName
  },
 
  // Payload original
  originalPayload: failedItem.originalInput,
 
  // Istoricul retry-urilor
  retryHistory: failedItem.retryHistory || [],
  totalRetries: failedItem.retryCount || 0,
 
  // Status
  status: 'unprocessed',
  priority: determinePriority(failedItem),
 
  // Metadata pentru 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 }];

Workflow de Procesare DLQ

// Function Node: Proceseaza Elementele din Dead Letter Queue
// Acest workflow ruleaza programat pentru a reincerca elementele DLQ
 
const dlqItems = $input.all().map(i => i.json);
const now = new Date();
 
// Configuratie
const config = {
  maxAgeHours: 72, // Elementele mai vechi de atat sunt arhivate
  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;
  }
 
  // Verifica daca se poate face retry
  if (config.retryableCategories.includes(item.error.category)) {
    toProcess.push(item);
  } else {
    // Nu se poate face retry - necesita interventie manuala
    if (item.status === 'unprocessed') {
      toArchive.push({
        ...item,
        status: 'requires_manual_review',
        reviewReason: `Categorie de eroare fara retry: ${item.error.category}`
        });
    }
  }
}
 
// Limiteaza dimensiunea batch-ului
const batch = toProcess.slice(0, config.batchSize);
 
return [
  ...batch.map(item => ({
    json: {
      action: 'retry',
      item
    }
  })),
  ...toArchive.map(item => ({
    json: {
      action: 'archive',
      item
    }
  }))
];

Alertare si Notificari

Managerul de Alerte

// Function Node: Manager Inteligent de Alerte
const error = $input.first().json;
 
// Configuratia alertelor
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'
    }
  },
  // Limitare rata
  rateLimits: {
    perWorkflow: {
      maxAlerts: 5,
      windowMinutes: 15
    },
    perError: {
      maxAlerts: 1,
      windowMinutes: 60
    }
  }
};
 
// Obtine alertele recente din static data
const alertHistory = $getWorkflowStaticData('global').alertHistory || [];
const now = Date.now();
 
// Curata alertele vechi
const recentAlerts = alertHistory.filter(a =>
  now - a.timestamp < 60 * 60 * 1000 // Ultima ora
);
 
// Verifica limitele de rata
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) {
  // Creeaza un hash al erorii pentru deduplicare
  return `${err.workflowId}_${err.nodeName}_${err.category}`;
}
 
// Determina pe ce canale se trimite alerta
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
    });
 
    // Inregistreaza alerta
    recentAlerts.push({
      timestamp: now,
      workflowId: error.workflowId,
      errorHash: hashError(error),
      channel
    });
  }
}
 
// Salveaza istoricul alertelor
$getWorkflowStaticData('global').alertHistory = recentAlerts;
 
return alerts.map(a => ({ json: a }));

Formatarea Alertelor Slack

// Function Node: Formateaza Alerta Slack
const alertData = $input.first().json;
const error = alertData.error;
 
const severityEmoji = {
  critical: '🔴',
  high: '🟠',
  medium: '🟡',
  low: '🟢'
}[error.severity] || '⚪';
 
const slackPayload = {
  text: `${severityEmoji} Eroare Workflow: ${error.workflowName}`,
  blocks: [
    {
      type: 'header',
      text: {
        type: 'plain_text',
        text: `${severityEmoji} Alerta Eroare Workflow`
      }
    },
    {
      type: 'section',
      fields: [
        {
          type: 'mrkdwn',
          text: `*Workflow:*\n${error.workflowName}`
        },
        {
          type: 'mrkdwn',
          text: `*Severitate:*\n${error.severity.toUpperCase()}`
        },
        {
          type: 'mrkdwn',
          text: `*Nod Esuat:*\n${error.nodeName}`
        },
        {
          type: 'mrkdwn',
          text: `*Categorie:*\n${error.category}`
        }
      ]
    },
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: `*Mesaj Eroare:*\n\`\`\`${error.message.substring(0, 500)}\`\`\``
      }
    },
    {
      type: 'context',
      elements: [
        {
          type: 'mrkdwn',
          text: `Execution ID: ${error.executionId} | Timp: ${error.timestamp}`
        }
      ]
    },
    {
      type: 'actions',
      elements: [
        {
          type: 'button',
          text: {
            type: 'plain_text',
            text: 'Vezi Executia'
          },
          url: `${$env.N8N_URL}/execution/${error.executionId}`
        },
        {
          type: 'button',
          text: {
            type: 'plain_text',
            text: 'Vezi Workflow-ul'
          },
          url: `${$env.N8N_URL}/workflow/${error.workflowId}`
        }
      ]
    }
  ]
};
 
return [{ json: slackPayload }];

Patternuri de Recuperare din Erori

Tranzactie Compensatoare

// Function Node: Handler Tranzactie Compensatoare
const failedOperation = $input.first().json;
 
// Urmareste pasii completati pentru rollback
const completedSteps = failedOperation.completedSteps || [];
 
// Defineste actiuni de compensare pentru fiecare pas
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
  })
};
 
// Construieste planul de compensare (ordine inversa)
const compensationPlan = completedSteps
  .reverse()
  .map(step => ({
    originalStep: step.name,
    compensationAction: compensationActions[step.name],
    stepData: step.data,
    status: 'pending'
  }))
  .filter(step => step.compensationAction); // Doar pasii cu compensare definita
 
return [{
  json: {
    failedAt: failedOperation.failedStep,
    compensationPlan,
    originalExecutionId: failedOperation.executionId
  }
}];

Implementarea Patternului Saga

// Function Node: Coordonator Saga
const sagaState = $input.first().json;
 
// Definirea pasilor saga
const sagaSteps = [
  {
    name: 'validate_request',
    action: 'validateOrder',
    compensation: null // Nu e nevoie de compensare
  },
  {
    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'
  }
];
 
// Initializeaza sau obtine starea saga
const saga = sagaState.saga || {
  id: `saga_${Date.now()}`,
  status: 'running',
  currentStep: 0,
  completedSteps: [],
  compensating: false,
  input: sagaState.input
};
 
// Determina urmatoarea actiune
let nextAction;
 
if (saga.compensating) {
  // Executa compensarea
  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 {
    // Toate compensarile complete
    saga.status = 'compensated';
    nextAction = {
      type: 'complete',
      status: 'rolled_back'
    };
  }
} else {
  // Executie normala
  if (saga.currentStep < sagaSteps.length) {
    const currentStepDef = sagaSteps[saga.currentStep];
    nextAction = {
      type: 'execute',
      step: currentStepDef.name,
      action: currentStepDef.action,
      data: saga.input
    };
  } else {
    // Toti pasii completi
    saga.status = 'completed';
    nextAction = {
      type: 'complete',
      status: 'success'
    };
  }
}
 
return [{
  json: {
    saga,
    nextAction
  }
}];

Sumar Bune Practici

  1. Categorizeaza erorile pentru strategii de tratare adecvate
  2. Foloseste exponential backoff cu jitter pentru retry-uri
  3. Implementeaza circuit breakers pentru servicii externe
  4. Mentine dead letter queues pentru elementele esuate
  5. Limiteaza rata alertelor pentru a preveni oboseala de notificari
  6. Proiecteaza tranzactii compensatoare pentru procese cu mai multi pasi
  7. Logheaza totul pentru debugging si analiza
  8. Testeaza scenariile de esec pentru a valida error handling-ul

Un error handling robust transforma workflow-urile fragile in automatizari reziliente, gata de productie. Investeste in error handling de la inceput pentru a preveni problemele la scara mare.

Ai nevoie de ajutor cu conformitatea EU AI Act sau securitatea AI?

Programeaza o consultatie gratuita de 30 de minute. Fara obligatii.

Programeaza un Apel

Weekly AI Security & Automation Digest

Get the latest on AI Security, workflow automation, secure integrations, and custom platform development delivered weekly.

No spam. Unsubscribe anytime.