Error Taxonomy Design: Turn Exceptions into Exceptional UX

8/13/2025
error-handling · api-design · user-experience · reliability
API Design11 min read4 hours to implement

TL;DR: Most error systems only document what went wrong. Great error systems guide users to what they should do next.

The Error Message That Cost 10,000 Users

A payment API returned this during Black Friday:

{
  "error": "INTERNAL_SERVER_ERROR", 
  "message": "Something went wrong"
}

10,000 checkout attempts failed. The real error? Rate limit exceeded. The fix? Wait 30 seconds. But the message revealed none of this.

Errors aren’t just failure notifications—they’re UX opportunities.

The Core Insight: Errors Are User Interfaces

Great error systems are conversation design. They should:

Mental Model: The Error Conversation Framework

Error Conversation:
1. ACKNOWLEDGE: "I understand what you tried to do"
2. EXPLAIN: "Here's what actually happened"  
3. GUIDE: "Here's how to fix it"
4. CONTEXT: "Here's how to debug if needed"

Implementation: From Cryptic Failures to Helpful Guidance

User-Action Taxonomy

enum UserErrorType {
  USER_FIXABLE = "USER_FIXABLE",          // User can fix immediately
  TEMPORARY_ISSUE = "TEMPORARY_ISSUE",     // User needs to wait/retry
  ACCESS_LIMITATION = "ACCESS_LIMITATION", // User needs different permissions
  SYSTEM_ISSUE = "SYSTEM_ISSUE"           // Contact support
}

interface StructuredError {
  code: string;           // Stable identifier
  type: UserErrorType;    // What user should do
  title: string;          // Human-readable explanation
  detail: string;
  
  userAction: {
    immediate: string;    // What to do right now
    followUp?: string;    // If immediate action fails
    documentation?: string;
  };
  
  debugInfo: {
    correlationId: string;
    timestamp: string;
    context?: Record<string, unknown>;
  };
  
  retryAfter?: number;    // Seconds to wait
  retryable: boolean;
}

Context-Aware Error Generation

// Error factory that includes user context
class ErrorFactory {
  static createPaymentError(reason: string, userContext: UserContext): StructuredError {
    if (reason === 'INSUFFICIENT_FUNDS') {
      return {
        code: 'PAYMENT_INSUFFICIENT_FUNDS',
        type: UserErrorType.USER_FIXABLE,
        title: 'Payment method declined',
        detail: `Your ${userContext.paymentMethod.type} ending in ${userContext.paymentMethod.last4} was declined for insufficient funds.`,
        
        userAction: {
          immediate: 'Try a different payment method or add funds to your account',
          followUp: 'Contact your bank if you believe this is an error'
        },
        
        retryable: true
      };
    }
    
    if (reason === 'RATE_LIMITED') {
      return {
        code: 'PAYMENT_RATE_LIMITED',
        type: UserErrorType.TEMPORARY_ISSUE,
        title: 'Too many payment attempts',
        detail: 'You\'ve exceeded the maximum number of payment attempts.',
        
        userAction: {
          immediate: 'Wait 5 minutes before trying again',
          followUp: 'Contact support if you continue having issues'
        },
        
        retryAfter: 300,
        retryable: true
      };
    }
  }
}

Real-World Impact: Stripe’s Error Excellence

Before: {"error": "card_declined", "message": "Your card was declined."}

After: Contextual, actionable errors with decline codes, recovery suggestions, and localized messages.

Results:

Your Error Excellence Action Plan

  1. Today: Audit your most common error message and rewrite it with user guidance
  2. This week: Add correlation IDs and retry semantics
  3. This month: Implement error analytics to continuously improve

Remember: Every error is a conversation with your user. Make it helpful.

References & Deep Dives