Error Handling

This guide provides comprehensive information about error handling patterns, error codes, and best practices for building robust ChatMinder integrations.

Overview

ChatMinder uses structured error responses with consistent patterns across all APIs, making it easier to build reliable error handling into your applications.

Error Response Structure

All API errors follow a consistent structure:

{
  "success": false,
  "message": "Human-readable error description",
  "errorCode": "MACHINE_READABLE_ERROR_CODE"
}

HTTP Status Codes

ChatMinder uses standard HTTP status codes to indicate different types of errors:

Status Code Meaning When Used
200 OK Success Request completed successfully
400 Bad Request Validation errors or invalid request Missing required fields, invalid data formats
401 Unauthorized Authentication required Missing or invalid JWT token
403 Forbidden Insufficient permissions User lacks permission for requested action
404 Not Found Resource not found Channel, user, or message doesn't exist
500 Internal Server Error Server error Unexpected server-side error

Error Code Categories

Authentication Errors

Error Code Description Common Causes
MISSING_AUTH_CODE Authorization code not provided OAuth callback missing code parameter
MISSING_REDIRECT_URI Redirect URI not provided OAuth request missing redirect URI
TOKEN_EXCHANGE_FAILED Failed to exchange auth code Invalid authorization code or expired
USER_INFO_FAILED Failed to get user info from provider Provider API error or invalid token
USER_CREATION_FAILED Account creation failed Database error or validation failure
OAUTH_NOT_CONFIGURED OAuth provider not configured Missing client ID/secret in server config

Guardian Authority Errors

Error Code Description Common Causes
UNAUTHORIZED_GUARDIAN_ACTION Guardian lacks authority for this user Trying to manage user not under guardianship
MESSAGE_ALREADY_PROCESSED Message already approved/rejected Duplicate approval attempt
CHANNEL_CREATION_DENIED User cannot create channels Protection level doesn't allow channel creation
CHANNEL_ACCESS_DENIED User cannot access channel User not a member or lacks permission

Validation Errors

Error Code Description Common Causes
INVALID_DEVICE_ID Device ID format invalid Push notification registration errors
INVALID_PLATFORM Platform not supported Unsupported device platform
INVALID_CHANNEL_ID Channel ID format invalid Negative numbers or malformed IDs
MISSING_FCM_TOKEN FCM token required Push notification setup incomplete

Real-time Connection Errors

Error Code Description Common Causes
ACCESS_DENIED SignalR access denied User tried to access unauthorized channel
INVALID_CHANNEL_ID Channel ID invalid in SignalR context Malformed channel ID in hub method
CONNECTION_FAILED SignalR connection failed Network issues or authentication problems

Error Handling Patterns

Basic Error Handling

async function apiCall(endpoint, options) {
    try {
        const response = await fetch(endpoint, options);
        
        if (!response.ok) {
            const error = await response.json();
            throw new APIError(error.message, error.errorCode, response.status);
        }
        
        return await response.json();
    } catch (error) {
        if (error instanceof APIError) {
            // Handle known API errors
            handleAPIError(error);
        } else {
            // Handle network or other unexpected errors
            console.error('Unexpected error:', error);
            throw new Error('An unexpected error occurred');
        }
    }
}

class APIError extends Error {
    constructor(message, errorCode, statusCode) {
        super(message);
        this.errorCode = errorCode;
        this.statusCode = statusCode;
        this.name = 'APIError';
    }
}

Authentication Error Handling

async function handleAuthenticationError(response) {
    if (!response.ok) {
        const error = await response.json();
        
        switch (error.errorCode) {
            case 'MISSING_AUTH_CODE':
            case 'MISSING_REDIRECT_URI':
                // Client-side configuration error
                showErrorMessage('OAuth configuration error. Please check your setup.');
                break;
                
            case 'TOKEN_EXCHANGE_FAILED':
                // OAuth flow interrupted or expired
                showErrorMessage('Authentication failed. Please try again.');
                redirectToLogin();
                break;
                
            case 'USER_INFO_FAILED':
                // Provider API issue
                showErrorMessage('Unable to retrieve account information.');
                break;
                
            case 'OAUTH_NOT_CONFIGURED':
                // Server configuration issue
                showErrorMessage('OAuth authentication is currently unavailable.');
                break;
                
            default:
                // General error handling
                showErrorMessage('Authentication failed. Please try again.');
        }
    }
}

Guardian Authority Error Handling

async function handleGuardianApproval(messageId, guardianToken) {
    try {
        const response = await fetch(`/api/guardian/pending-messages/${messageId}/approve`, {
            method: 'POST',
            headers: { 'Authorization': `Bearer ${guardianToken}` }
        });
        
        if (!response.ok) {
            const error = await response.json();
            switch (error.errorCode) {
                case 'UNAUTHORIZED_GUARDIAN_ACTION':
                    showErrorMessage('You do not have authority to manage this user.');
                    return false;
                    
                case 'MESSAGE_ALREADY_PROCESSED':
                    showWarningMessage('This message has already been processed.');
                    return false;
                    
                default:
                    showErrorMessage(`Approval failed: ${error.message}`);
                    return false;
            }
        }
        
        showSuccessMessage('Message approved successfully.');
        return true;
    } catch (error) {
        console.error('Network error during approval:', error);
        showErrorMessage('Network error. Please check your connection and try again.');
        return false;
    }
}

SignalR Error Handling

// Set up comprehensive SignalR error handling
const connection = new HubConnectionBuilder()
    .withUrl('/chatHub', { accessTokenFactory: () => getJwtToken() })
    .withAutomaticReconnect()
    .build();

// Handle SignalR-specific errors
connection.on('ErrorOccurred', (error) => {
    switch(error) {
        case 'ACCESS_DENIED':
            showErrorMessage('You do not have access to this channel.');
            break;
            
        case 'INVALID_CHANNEL_ID':
            console.error('Invalid channel ID provided to SignalR hub');
            break;
            
        default:
            console.error('SignalR Error:', error);
            showErrorMessage('A real-time communication error occurred.');
    }
});

// Handle connection failures
connection.onclose((error) => {
    if (error) {
        console.error('SignalR connection lost:', error);
        showWarningMessage('Real-time connection lost. Attempting to reconnect...');
        
        // Implement custom reconnection logic if needed
        setTimeout(() => {
            connection.start().catch(err => {
                console.error('Reconnection failed:', err);
                showErrorMessage('Unable to restore real-time connection. Please refresh the page.');
            });
        }, 5000);
    }
});

Error Recovery Strategies

Automatic Retry Logic

async function retryableRequest(url, options, maxRetries = 3) {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            const response = await fetch(url, options);
            
            if (response.ok) {
                return await response.json();
            }
            
            // Don't retry on client errors (4xx)
            if (response.status >= 400 && response.status < 500) {
                throw new APIError('Client error', 'CLIENT_ERROR', response.status);
            }
            
            // Retry on server errors (5xx) if we haven't exceeded max attempts
            if (attempt < maxRetries) {
                const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
                await new Promise(resolve => setTimeout(resolve, delay));
                continue;
            }
            
            throw new APIError('Server error', 'SERVER_ERROR', response.status);
            
        } catch (error) {
            if (attempt === maxRetries) {
                throw error;
            }
            
            // Wait before retrying
            const delay = Math.pow(2, attempt) * 1000;
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
}

Graceful Degradation

class ChatManager {
    constructor() {
        this.isOnline = true;
        this.queuedMessages = [];
        this.retryInterval = null;
    }
    
    async sendMessage(channelId, content) {
        try {
            const response = await fetch(`/api/messages/channel/${channelId}`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ content, messageType: 'text' })
            });
            
            if (response.ok) {
                this.isOnline = true;
                this.processQueuedMessages(); // Process any queued messages
                return await response.json();
            }
            
        } catch (error) {
            // Network error - queue message for later
            this.isOnline = false;
            this.queueMessage(channelId, content);
            this.startRetryTimer();
            
            showWarningMessage('Message queued. Will send when connection is restored.');
        }
    }
    
    queueMessage(channelId, content) {
        this.queuedMessages.push({ channelId, content, timestamp: Date.now() });
    }
    
    async processQueuedMessages() {
        while (this.queuedMessages.length > 0 && this.isOnline) {
            const message = this.queuedMessages.shift();
            try {
                await this.sendMessage(message.channelId, message.content);
            } catch (error) {
                // Re-queue if still failing
                this.queuedMessages.unshift(message);
                break;
            }
        }
    }
    
    startRetryTimer() {
        if (this.retryInterval) return;
        
        this.retryInterval = setInterval(async () => {
            if (this.queuedMessages.length > 0) {
                await this.processQueuedMessages();
            }
            
            if (this.queuedMessages.length === 0) {
                clearInterval(this.retryInterval);
                this.retryInterval = null;
            }
        }, 10000); // Retry every 10 seconds
    }
}

User Experience Best Practices

Error Message Guidelines

Do:

  • ✅ Use clear, non-technical language
  • ✅ Provide actionable next steps
  • ✅ Distinguish between temporary and permanent errors
  • ✅ Show progress for retry attempts

Don't:

  • ❌ Expose internal error codes to end users
  • ❌ Use technical jargon
  • ❌ Show stack traces in production
  • ❌ Leave users without guidance

Error Message Examples

// Good error messages
const userFriendlyMessages = {
    'UNAUTHORIZED_GUARDIAN_ACTION': 'You don\'t have permission to manage this user.',
    'MESSAGE_ALREADY_PROCESSED': 'This message has already been reviewed.',
    'CONNECTION_FAILED': 'Unable to connect. Please check your internet connection.',
    'TOKEN_EXCHANGE_FAILED': 'Sign-in failed. Please try signing in again.',
    'INVALID_DEVICE_ID': 'There was a problem setting up notifications. Please try again.'
};

function showUserFriendlyError(errorCode, fallbackMessage) {
    const message = userFriendlyMessages[errorCode] || fallbackMessage;
    showErrorToast(message);
}

Loading States and Error Recovery

// UI state management during API calls
async function performAction(actionFn, loadingMessage = 'Processing...') {
    showLoadingState(loadingMessage);
    
    try {
        const result = await actionFn();
        showSuccessState();
        return result;
    } catch (error) {
        if (error instanceof APIError) {
            showErrorState(error.message, () => {
                // Retry button
                performAction(actionFn, loadingMessage);
            });
        } else {
            showErrorState('An unexpected error occurred', () => {
                window.location.reload();
            });
        }
        throw error;
    }
}

// Usage
await performAction(
    () => approveMessage(messageId), 
    'Approving message...'
);

Debugging and Monitoring

Client-Side Error Logging

class ErrorLogger {
    static log(error, context = {}) {
        const errorData = {
            timestamp: new Date().toISOString(),
            message: error.message,
            errorCode: error.errorCode,
            statusCode: error.statusCode,
            stack: error.stack,
            context: context,
            userAgent: navigator.userAgent,
            url: window.location.href
        };
        
        // Log to console in development
        if (process.env.NODE_ENV === 'development') {
            console.error('Error logged:', errorData);
        }
        
        // Send to monitoring service in production
        if (process.env.NODE_ENV === 'production') {
            this.sendToMonitoring(errorData);
        }
    }
    
    static async sendToMonitoring(errorData) {
        try {
            await fetch('/api/errors/log', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(errorData)
            });
        } catch (err) {
            // Fail silently for error logging
            console.warn('Failed to send error to monitoring:', err);
        }
    }
}

// Usage
try {
    await riskyOperation();
} catch (error) {
    ErrorLogger.log(error, { operation: 'message-approval', userId: currentUserId });
    throw error;
}

Testing Error Scenarios

Mock Error Responses

// Testing error handling in Jest
const mockErrorResponse = (errorCode, message, statusCode = 400) => {
    return Promise.resolve({
        ok: false,
        status: statusCode,
        json: () => Promise.resolve({ 
            success: false, 
            errorCode, 
            message 
        })
    });
};

test('handles guardian authority error', async () => {
    global.fetch = jest.fn(() => 
        mockErrorResponse('UNAUTHORIZED_GUARDIAN_ACTION', 'Access denied', 403)
    );
    
    const result = await handleGuardianApproval('msg123', 'token');
    
    expect(result).toBe(false);
    expect(console.error).toHaveBeenCalledWith(
        'Guardian does not have authority for this user'
    );
});