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 NecunoscuteStrategii 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
- Categorizeaza erorile pentru strategii de tratare adecvate
- Foloseste exponential backoff cu jitter pentru retry-uri
- Implementeaza circuit breakers pentru servicii externe
- Mentine dead letter queues pentru elementele esuate
- Limiteaza rata alertelor pentru a preveni oboseala de notificari
- Proiecteaza tranzactii compensatoare pentru procese cu mai multi pasi
- Logheaza totul pentru debugging si analiza
- 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.