Skip to content

Error Handling Architecture ​

Technical overview of the error handling system architecture.

Design Principles ​

1. TypeScript as Source of Truth ​

All error classification happens in TypeScript, not in Python or any other language.

Why?

  • Unified error handling across all services (ComfyUI, OpenAI, Gemini, etc.)
  • Minimal changes to upstream dependencies (ComfyUI fork)
  • Faster iteration (no Docker rebuilds for ComfyUI)
  • Single language/ecosystem for maintenance

2. Fail with Clarity, Not Fallbacks ​

From CLAUDE.md:

ALWAYS favor descriptive errors over fallbacks... ALWAYS

  • No silent failures
  • Explicit error messages with actionable suggestions
  • Fail fast with clear root cause visibility

3. Two-Tier Classification ​

FailureType (High-Level)  →  FailureReason (Specific)
      ↓                              ↓
VALIDATION_ERROR         MISSING_REQUIRED_FIELD
RESOURCE_LIMIT          GPU_MEMORY_FULL
MODEL_ERROR             MODEL_NOT_FOUND

4. Critical vs Non-Critical Filtering ​

Not all errors should fail jobs:

  • Critical: Workflow execution errors → Fail job
  • Non-Critical: Infrastructure/telemetry errors → Log only

System Components ​

Python Layer (ComfyUI) ​

Location: packages/comfyui/execution.py

Responsibility: Catch exceptions and send raw data only

python
# Send RAW exception info only (no classification)
error_details = {
    "node_id": real_node_id,
    "node_type": class_type,
    "exception_message": str(ex),
    "exception_type": exception_type,
    "traceback": traceback.format_tb(tb),
    "current_inputs": input_data_formatted,
}

# Publish to Redis Stream
redis.xadd('comfyui:unified:events', {
    'event_type': 'log',
    'level': 'error',
    'message': json.dumps(error_details)
})

What it does NOT do:

  • ❌ Classify errors
  • ❌ Determine retryability
  • ❌ Generate user messages
  • ❌ Pattern matching

TypeScript Worker (Connector) ​

Location: apps/worker/src/connectors/comfyui-rest-stream-connector.ts

Responsibility: Filter and route errors

typescript
// Filter 1: Log Level
const isCriticalLevel = logLevel === 'critical' || logLevel === 'error';
if (!isCriticalLevel) {
  return { type: 'timeout' }; // Skip warning/info/debug
}

// Filter 2: Infrastructure Errors
const isInfrastructureError = this.isInfrastructureError(logMessage);
if (isInfrastructureError) {
  logger.warn(`⚙️ Infrastructure error (not failing job)`);
  return { type: 'timeout' }; // Log but don't fail
}

// Critical error → Send to enhancer
const connectorError = ComfyUIErrorEnhancer.enhance(error, context);

TypeScript Core (Enhancer) ​

Location: packages/core/src/errors/comfyui-error-enhancer.ts

Responsibility: Pattern matching and classification

typescript
static enhance(error: ComfyUIError, context?: ComfyUIErrorContext): ConnectorError {
  const message = error.exception_message || '';
  const exceptionType = error.exception_type || '';

  // Pattern matching in order (specific → generic)
  if (message.match(/node .+ does not exist/i)) {
    // Missing custom node
    return new ConnectorError(
      FailureType.VALIDATION_ERROR,
      FailureReason.UNSUPPORTED_OPERATION,
      'Custom node not installed',
      false,
      { suggestion: 'Install the custom node' }
    );
  }

  if (exceptionType.toLowerCase() === 'keyerror') {
    // Missing required field
    return new ConnectorError(
      FailureType.VALIDATION_ERROR,
      FailureReason.MISSING_REQUIRED_FIELD,
      `Missing required field '${fieldName}'`,
      false,
      { suggestion: 'Provide the required parameter' }
    );
  }

  // Fallback to FailureClassifier
  return FailureClassifier.classify(message);
}

Fallback Classifier ​

Location: packages/core/src/types/failure-classification.ts

Responsibility: Generic pattern matching for all services

typescript
export class FailureClassifier {
  static classify(errorMessage: string, context?: ClassificationContext) {
    const error = errorMessage.toLowerCase();

    // GPU OOM
    if (error.includes('cuda out of memory')) {
      return {
        failure_type: FailureType.RESOURCE_LIMIT,
        failure_reason: FailureReason.GPU_MEMORY_FULL,
        failure_description: 'GPU ran out of memory',
        retryable: true
      };
    }

    // ... more patterns ...

    // Unknown
    return {
      failure_type: FailureType.SYSTEM_ERROR,
      failure_reason: FailureReason.UNKNOWN_ERROR,
      failure_description: errorMessage,
      retryable: false
    };
  }
}

ConnectorError ​

Location: packages/core/src/types/connector-errors.ts

Responsibility: Structured error object

typescript
class ConnectorError extends Error {
  constructor(
    public failureType: FailureType,
    public failureReason: FailureReason,
    public message: string,
    public retryable: boolean,
    public context: Record<string, any>
  ) {
    super(message);
  }
}

Data Flow ​

1. Exception Occurs ​

2. Worker Processing ​

3. UI Display ​

Error Flow Example ​

Scenario: Missing URL Parameter ​

Step 1: Python Exception

python
# packages/comfyui/custom_nodes/emprops_comfy_nodes/nodes/emprops_image_loader.py
def load_image(self, **kwargs):
    url = kwargs['url']  # KeyError: 'url'

Step 2: Exception Handler

python
# packages/comfyui/execution.py
except Exception as ex:
    error_details = {
        "node_id": "66",
        "node_type": "emprops_image_loader",
        "exception_message": "KeyError: 'url'",
        "exception_type": "KeyError",
        "traceback": [...],
        "current_inputs": {}
    }
    # Publish to stream

Step 3: Worker Receives

typescript
// apps/worker/src/connectors/comfyui-rest-stream-connector.ts
const logResult = await this.readLogStream(...);

// Filter 1: logLevel === 'error' ✅ (passes)
// Filter 2: not infrastructure ✅ (passes)

// Send to enhancer
const connectorError = ComfyUIErrorEnhancer.enhance(error);

Step 4: Pattern Matching

typescript
// packages/core/src/errors/comfyui-error-enhancer.ts
if (exceptionType.toLowerCase() === 'keyerror') {
  const fieldMatch = message.match(/KeyError:\s*['"]([^'"]+)['"]/i);
  const fieldName = fieldMatch[1]; // 'url'

  return new ConnectorError(
    FailureType.VALIDATION_ERROR,
    FailureReason.MISSING_REQUIRED_FIELD,
    `Missing required field 'url' in node emprops_image_loader`,
    false,
    {
      suggestion: "Provide the required 'url' parameter in your workflow configuration",
      missingField: 'url'
    }
  );
}

Step 5: Job Fails

typescript
// Job marked as failed
// User sees in Monitor UI:
// ❌ Missing required field 'url' in node emprops_image_loader
// Suggestion: Provide the required 'url' parameter in your workflow configuration

Extension Points ​

Adding New Service Error Handlers ​

typescript
// Create service-specific enhancer
export class OpenAIErrorEnhancer {
  static enhance(error: OpenAIError): ConnectorError {
    // Service-specific pattern matching
  }
}

// Use in connector
class OpenAIConnector extends BaseConnector {
  protected handleServiceError(error: any): ConnectorError {
    return OpenAIErrorEnhancer.enhance(error);
  }
}

Adding New FailureReason ​

typescript
// packages/core/src/types/failure-classification.ts
export enum FailureReason {
  // Existing...
  GPU_MEMORY_FULL = 'gpu_memory_full',

  // New
  VRAM_EXCEEDED = 'vram_exceeded',  // Add this
}

// packages/core/src/types/connector-errors.ts
export const ErrorDescriptions: Record<FailureReason, string> = {
  // Existing...
  [FailureReason.GPU_MEMORY_FULL]: 'GPU ran out of memory',

  // New
  [FailureReason.VRAM_EXCEEDED]: 'VRAM allocation limit exceeded',
};

Performance Considerations ​

Pattern Matching Order ​

typescript
// ✅ Specific patterns first (fast path)
if (message.match(/node .+ does not exist/i)) { ... }

// ✅ Common patterns second
if (exceptionType === 'keyerror') { ... }

// ✅ Expensive regex last
if (message.match(/complex.*regex.*pattern/i)) { ... }

// ✅ Fallback (slowest)
return FailureClassifier.classify(message);

Caching ​

Currently no caching - each error is pattern-matched fresh.

Future optimization: Cache pattern → ConnectorError mapping

typescript
// Potential future optimization
const errorCache = new LRU<string, ConnectorError>(100);

static enhance(error: ComfyUIError): ConnectorError {
  const cacheKey = `${error.exception_type}:${error.exception_message}`;

  if (errorCache.has(cacheKey)) {
    return errorCache.get(cacheKey);
  }

  const result = /* pattern matching */;
  errorCache.set(cacheKey, result);
  return result;
}

Testing Strategy ​

Unit Tests ​

typescript
// Test individual pattern matchers
describe('ComfyUIErrorEnhancer', () => {
  it('should classify KeyError', () => {
    const error = { exception_type: 'KeyError', exception_message: "KeyError: 'url'" };
    const result = ComfyUIErrorEnhancer.enhance(error);
    expect(result.failureReason).toBe('missing_required_field');
  });
});

Integration Tests ​

bash
# Test full error flow (Python → TypeScript → UI)
1. Start local development environment
2. Trigger error in ComfyUI workflow
3. Verify error classification in worker logs
4. Verify error display in Monitor UI

E2E Tests ​

typescript
// Test error handling across services
it('should handle ComfyUI KeyError end-to-end', async () => {
  const jobId = await submitJob({ /* missing url */ });

  await waitForJobCompletion(jobId);

  const job = await getJob(jobId);
  expect(job.status).toBe('failed');
  expect(job.error.failureReason).toBe('missing_required_field');
  expect(job.error.context.missingField).toBe('url');
});

Monitoring ​

Metrics to Track ​

  • Error classification accuracy (% matched vs fallback)
  • Pattern matching performance (avg time)
  • Infrastructure error filter rate (% skipped)
  • Most common error patterns

Logging ​

typescript
// Debug logging for pattern matching
logger.debug('Classifying error:', {
  exceptionType: error.exception_type,
  messagePreview: error.exception_message.substring(0, 100),
  matchedPattern: 'KeyError',
  failureReason: 'missing_required_field'
});

// Info logging for infrastructure errors
logger.info('Infrastructure error detected (not failing job):', {
  message: logMessage.substring(0, 200),
  category: 'OpenTelemetry Export'
});

Future Improvements ​

Machine Learning Classification ​

typescript
// Potential ML-based classification
class MLErrorClassifier {
  async classify(error: string): Promise<ConnectorError> {
    const embedding = await getEmbedding(error);
    const prediction = await model.predict(embedding);

    return new ConnectorError(
      prediction.failureType,
      prediction.failureReason,
      prediction.message,
      prediction.retryable,
      { confidence: prediction.confidence }
    );
  }
}

Error Pattern Analytics ​

typescript
// Track pattern effectiveness
interface PatternStats {
  pattern: string;
  matchCount: number;
  lastMatched: Date;
  avgConfidence: number;
}

// Identify gaps in coverage
const unmatchedErrors = errors.filter(e =>
  e.failureReason === FailureReason.UNKNOWN_ERROR
);

Auto-Retry Strategy ​

typescript
// Smart retry based on classification
if (error.failureType === FailureType.RESOURCE_LIMIT && error.retryable) {
  // Retry with reduced settings
  await retryWithReducedBatchSize(job);
} else if (error.failureType === FailureType.TIMEOUT) {
  // Retry with increased timeout
  await retryWithIncreasedTimeout(job);
}

Released under the MIT License.