Error Handling and Logging Best Practices Across Languages

Error Handling and Logging Best Practices Across Languages

Introduction

Robust error handling and effective logging are critical for production applications. This guide covers exception handling patterns, custom exception hierarchies, structured logging with correlation tracking, and modern error monitoring practices across JavaScript, Python, and C#.

Error Handling Fundamentals

Try-Catch-Finally Structure

JavaScript:

async function fetchUserData(userId) {
    try {
        const response = await fetch(`/api/users/${userId}`);
        
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Failed to fetch user:', error.message);
        throw error;  // Re-throw for caller to handle
    } finally {
        console.log('Fetch attempt completed');
    }
}

Python:

def read_config_file(filename):
    try:
        with open(filename, 'r') as file:
            config = json.load(file)
            return config
    except FileNotFoundError:
        print(f"Config file not found: {filename}")
        return {}
    except json.JSONDecodeError as e:
        print(f"Invalid JSON in config file: {e}")
        return {}
    except Exception as e:
        print(f"Unexpected error reading config: {e}")
        raise
    finally:
        print("Config read attempt completed")

C#:

public async Task<User> GetUserAsync(int userId)
{
    HttpResponseMessage response = null;
    try
    {
        response = await _httpClient.GetAsync($"/api/users/{userId}");
        response.EnsureSuccessStatusCode();
        
        var user = await response.Content.ReadFromJsonAsync<User>();
        return user;
    }
    catch (HttpRequestException ex)
    {
        _logger.LogError(ex, "HTTP error fetching user {UserId}", userId);
        throw;
    }
    catch (JsonException ex)
    {
        _logger.LogError(ex, "JSON parsing error for user {UserId}", userId);
        throw;
    }
    finally
    {
        response?.Dispose();
    }
}

Specific vs. Generic Exception Handling

❌ Bad - Catching all exceptions:

try:
    result = risky_operation()
except:  # Catches everything including KeyboardInterrupt!
    print("Something went wrong")

✅ Good - Specific exception handling:

try:
    result = process_payment(amount)
except PaymentDeclinedError as e:
    notify_user(f"Payment declined: {e.reason}")
except InsufficientFundsError as e:
    notify_user(f"Insufficient funds. Available: {e.available}")
except PaymentProviderError as e:
    log_error(f"Provider error: {e}")
    retry_payment(amount)
except Exception as e:
    log_critical(f"Unexpected payment error: {e}")
    raise

Custom Exception Hierarchies

Python Example

class ApplicationError(Exception):
    """Base exception for application errors."""
    def __init__(self, message, code=None, details=None):
        super().__init__(message)
        self.code = code
        self.details = details or {}
        self.timestamp = datetime.utcnow()

class ValidationError(ApplicationError):
    """Raised when data validation fails."""
    def __init__(self, field, message, value=None):
        super().__init__(
            message=f"Validation failed for {field}: {message}",
            code="VALIDATION_ERROR",
            details={"field": field, "value": value}
        )
        self.field = field

class ResourceNotFoundError(ApplicationError):
    """Raised when a requested resource doesn't exist."""
    def __init__(self, resource_type, resource_id):
        super().__init__(
            message=f"{resource_type} with ID {resource_id} not found",
            code="RESOURCE_NOT_FOUND",
            details={"resource_type": resource_type, "resource_id": resource_id}
        )

class AuthenticationError(ApplicationError):
    """Raised when authentication fails."""
    def __init__(self, message="Authentication failed"):
        super().__init__(message, code="AUTH_ERROR")

class DatabaseError(ApplicationError):
    """Raised when database operations fail."""
    def __init__(self, operation, original_error):
        super().__init__(
            message=f"Database {operation} failed: {str(original_error)}",
            code="DB_ERROR",
            details={"operation": operation}
        )

# Usage
def get_user(user_id):
    if not isinstance(user_id, int):
        raise ValidationError("user_id", "Must be an integer", user_id)
    
    try:
        user = db.query(User).filter_by(id=user_id).first()
    except SQLAlchemyError as e:
        raise DatabaseError("query", e) from e
    
    if user is None:
        raise ResourceNotFoundError("User", user_id)
    
    return user

# Error handling
try:
    user = get_user("invalid")
except ValidationError as e:
    return {"error": e.code, "message": str(e), "details": e.details}
except ResourceNotFoundError as e:
    return {"error": e.code, "message": str(e)}, 404
except DatabaseError as e:
    logger.error(f"Database error: {e}", exc_info=True)
    return {"error": "INTERNAL_ERROR", "message": "Service unavailable"}, 503

C# Example

// Base exception
public abstract class ApplicationException : Exception
{
    public string Code { get; }
    public Dictionary<string, object> Details { get; }
    
    protected ApplicationException(
        string message,
        string code,
        Exception innerException = null)
        : base(message, innerException)
    {
        Code = code;
        Details = new Dictionary<string, object>();
    }
}

// Specific exceptions
public class ValidationException : ApplicationException
{
    public string Field { get; }
    
    public ValidationException(string field, string message, object value = null)
        : base($"Validation failed for {field}: {message}", "VALIDATION_ERROR")
    {
        Field = field;
        if (value != null)
            Details["value"] = value;
    }
}

public class ResourceNotFoundException : ApplicationException
{
    public ResourceNotFoundException(string resourceType, object resourceId)
        : base($"{resourceType} with ID {resourceId} not found", "RESOURCE_NOT_FOUND")
    {
        Details["resourceType"] = resourceType;
        Details["resourceId"] = resourceId;
    }
}

public class ExternalServiceException : ApplicationException
{
    public string ServiceName { get; }
    public int? StatusCode { get; }
    
    public ExternalServiceException(
        string serviceName,
        string message,
        int? statusCode = null,
        Exception innerException = null)
        : base($"{serviceName} error: {message}", "EXTERNAL_SERVICE_ERROR", innerException)
    {
        ServiceName = serviceName;
        StatusCode = statusCode;
    }
}

// Usage with exception filters (C# 6+)
public async Task<User> GetUserAsync(int userId)
{
    try
    {
        if (userId <= 0)
            throw new ValidationException("userId", "Must be positive", userId);
        
        var user = await _context.Users.FindAsync(userId);
        
        if (user == null)
            throw new ResourceNotFoundException("User", userId);
        
        return user;
    }
    catch (DbUpdateException ex) when (ex.InnerException is SqlException sqlEx)
    {
        _logger.LogError(ex, "Database error fetching user {UserId}", userId);
        throw new ApplicationException("Database operation failed", "DB_ERROR", ex);
    }
}

TypeScript Example

// Base error class
class ApplicationError extends Error {
    constructor(
        message: string,
        public code: string,
        public statusCode: number = 500,
        public details?: Record<string, any>
    ) {
        super(message);
        this.name = this.constructor.name;
        Error.captureStackTrace(this, this.constructor);
    }
}

// Specific errors
class ValidationError extends ApplicationError {
    constructor(field: string, message: string, value?: any) {
        super(
            `Validation failed for ${field}: ${message}`,
            'VALIDATION_ERROR',
            400,
            { field, value }
        );
    }
}

class NotFoundError extends ApplicationError {
    constructor(resource: string, id: string | number) {
        super(
            `${resource} with ID ${id} not found`,
            'NOT_FOUND',
            404,
            { resource, id }
        );
    }
}

class UnauthorizedError extends ApplicationError {
    constructor(message: string = 'Unauthorized access') {
        super(message, 'UNAUTHORIZED', 401);
    }
}

// Express error handler middleware
function errorHandler(
    err: Error,
    req: express.Request,
    res: express.Response,
    next: express.NextFunction
) {
    if (err instanceof ApplicationError) {
        return res.status(err.statusCode).json({
            error: err.code,
            message: err.message,
            details: err.details
        });
    }
    
    // Unexpected errors
    logger.error('Unexpected error:', err);
    res.status(500).json({
        error: 'INTERNAL_ERROR',
        message: 'An unexpected error occurred'
    });
}

Retry Patterns

Python Retry with Exponential Backoff

import time
import random
from functools import wraps

def retry_with_backoff(
    max_attempts=3,
    initial_delay=1,
    max_delay=60,
    exponential_base=2,
    jitter=True,
    exceptions=(Exception,)
):
    """Decorator for retrying functions with exponential backoff."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            delay = initial_delay
            
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        raise
                    
                    # Calculate delay with exponential backoff
                    delay = min(delay * exponential_base, max_delay)
                    
                    # Add jitter to prevent thundering herd
                    if jitter:
                        delay = delay * (0.5 + random.random())
                    
                    logger.warning(
                        f"Attempt {attempt}/{max_attempts} failed: {e}. "
                        f"Retrying in {delay:.2f}s..."
                    )
                    time.sleep(delay)
            
        return wrapper
    return decorator

# Usage
@retry_with_backoff(max_attempts=5, exceptions=(requests.RequestException,))
def fetch_data(url):
    response = requests.get(url, timeout=10)
    response.raise_for_status()
    return response.json()

# Async version
async def async_retry_with_backoff(
    coro_func,
    max_attempts=3,
    initial_delay=1,
    exceptions=(Exception,)
):
    """Retry async function with exponential backoff."""
    delay = initial_delay
    
    for attempt in range(1, max_attempts + 1):
        try:
            return await coro_func()
        except exceptions as e:
            if attempt == max_attempts:
                raise
            
            delay *= 2
            logger.warning(f"Attempt {attempt} failed: {e}. Retrying in {delay}s...")
            await asyncio.sleep(delay)

C# with Polly

using Polly;
using Polly.Retry;

public class ResilientHttpClient
{
    private readonly HttpClient _httpClient;
    private readonly AsyncRetryPolicy<HttpResponseMessage> _retryPolicy;
    private readonly ILogger<ResilientHttpClient> _logger;
    
    public ResilientHttpClient(HttpClient httpClient, ILogger<ResilientHttpClient> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
        
        _retryPolicy = Policy
            .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
            .Or<HttpRequestException>()
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: retryAttempt =>
                    TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)),
                onRetry: (outcome, timespan, retryCount, context) =>
                {
                    _logger.LogWarning(
                        "Request failed with {StatusCode}. Waiting {Delay}s before retry {Retry}",
                        outcome.Result?.StatusCode,
                        timespan.TotalSeconds,
                        retryCount
                    );
                }
            );
    }
    
    public async Task<T> GetAsync<T>(string url)
    {
        var response = await _retryPolicy.ExecuteAsync(async () =>
            await _httpClient.GetAsync(url)
        );
        
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadFromJsonAsync<T>();
    }
}

Structured Logging

Python with Structlog

import structlog
from contextvars import ContextVar

# Context variables for request tracking
request_id_var: ContextVar[str] = ContextVar('request_id', default='')
user_id_var: ContextVar[str] = ContextVar('user_id', default='')

# Configure structlog
structlog.configure(
    processors=[
        structlog.contextvars.merge_contextvars,
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.processors.JSONRenderer()
    ],
    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
)

logger = structlog.get_logger()

# Middleware to set context
def logging_middleware(request):
    request_id = request.headers.get('X-Request-ID', str(uuid.uuid4()))
    user_id = getattr(request.user, 'id', 'anonymous')
    
    request_id_var.set(request_id)
    user_id_var.set(user_id)
    
    structlog.contextvars.clear_contextvars()
    structlog.contextvars.bind_contextvars(
        request_id=request_id,
        user_id=user_id,
        path=request.path,
        method=request.method
    )
    
    logger.info("request_started")
    
    try:
        response = process_request(request)
        logger.info("request_completed", status_code=response.status_code)
        return response
    except Exception as e:
        logger.error("request_failed", error=str(e), exc_info=True)
        raise

# Usage in application code
def create_order(user_id, items):
    logger.info("creating_order", item_count=len(items))
    
    try:
        total = calculate_total(items)
        logger.debug("order_total_calculated", total=total)
        
        order = Order.objects.create(user_id=user_id, total=total)
        logger.info("order_created", order_id=order.id, total=total)
        
        return order
    except ValidationError as e:
        logger.warning("order_validation_failed", errors=e.details)
        raise
    except Exception as e:
        logger.error("order_creation_failed", error=str(e), exc_info=True)
        raise

C# with Serilog

using Serilog;
using Serilog.Context;

// Configure Serilog
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .Enrich.FromLogContext()
    .Enrich.WithProperty("Application", "MyApp")
    .Enrich.WithMachineName()
    .WriteTo.Console(new JsonFormatter())
    .WriteTo.File(
        new JsonFormatter(),
        "logs/app-.log",
        rollingInterval: RollingInterval.Day,
        retainedFileCountLimit: 30
    )
    .CreateLogger();

// Middleware for correlation ID
public class CorrelationIdMiddleware
{
    private readonly RequestDelegate _next;
    
    public async Task InvokeAsync(HttpContext context)
    {
        var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault()
            ?? Guid.NewGuid().ToString();
        
        using (LogContext.PushProperty("CorrelationId", correlationId))
        using (LogContext.PushProperty("UserId", context.User?.Identity?.Name))
        {
            context.Response.Headers.Add("X-Correlation-ID", correlationId);
            
            Log.Information(
                "Request started {Method} {Path}",
                context.Request.Method,
                context.Request.Path
            );
            
            try
            {
                await _next(context);
                
                Log.Information(
                    "Request completed {StatusCode}",
                    context.Response.StatusCode
                );
            }
            catch (Exception ex)
            {
                Log.Error(ex, "Request failed");
                throw;
            }
        }
    }
}

// Service usage
public class OrderService
{
    private readonly ILogger<OrderService> _logger;
    
    public async Task<Order> CreateOrderAsync(int userId, List<OrderItem> items)
    {
        using (_logger.BeginScope(new Dictionary<string, object>
        {
            ["UserId"] = userId,
            ["ItemCount"] = items.Count
        }))
        {
            _logger.LogInformation("Creating order");
            
            try
            {
                var total = items.Sum(i => i.Price * i.Quantity);
                _logger.LogDebug("Order total calculated: {Total}", total);
                
                var order = new Order { UserId = userId, Total = total };
                await _context.Orders.AddAsync(order);
                await _context.SaveChangesAsync();
                
                _logger.LogInformation(
                    "Order created successfully {OrderId}",
                    order.Id
                );
                
                return order;
            }
            catch (DbUpdateException ex)
            {
                _logger.LogError(ex, "Database error creating order");
                throw;
            }
        }
    }
}

Production Error Monitoring

Application Insights (Azure)

using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;

public class PaymentService
{
    private readonly TelemetryClient _telemetry;
    
    public async Task ProcessPaymentAsync(Payment payment)
    {
        var operation = _telemetry.StartOperation<RequestTelemetry>("ProcessPayment");
        operation.Telemetry.Properties["PaymentMethod"] = payment.Method;
        operation.Telemetry.Properties["Amount"] = payment.Amount.ToString();
        
        try
        {
            await _paymentGateway.ChargeAsync(payment);
            
            _telemetry.TrackEvent("PaymentSucceeded", new Dictionary<string, string>
            {
                ["PaymentId"] = payment.Id.ToString(),
                ["Amount"] = payment.Amount.ToString()
            });
            
            operation.Telemetry.Success = true;
        }
        catch (PaymentDeclinedException ex)
        {
            _telemetry.TrackException(ex, new Dictionary<string, string>
            {
                ["PaymentId"] = payment.Id.ToString(),
                ["DeclineReason"] = ex.Reason
            });
            
            operation.Telemetry.Success = false;
            throw;
        }
        finally
        {
            _telemetry.StopOperation(operation);
        }
    }
}

Sentry (JavaScript)

import * as Sentry from '@sentry/node';

Sentry.init({
    dsn: process.env.SENTRY_DSN,
    environment: process.env.NODE_ENV,
    tracesSampleRate: 1.0,
    integrations: [
        new Sentry.Integrations.Http({ tracing: true }),
    ],
});

// Express error handler
app.use(Sentry.Handlers.errorHandler());

// Manual error tracking
async function processOrder(orderId: string): Promise<void> {
    const transaction = Sentry.startTransaction({
        op: 'process_order',
        name: 'Process Order',
    });
    
    try {
        Sentry.setContext('order', { orderId });
        
        const order = await fetchOrder(orderId);
        await validateOrder(order);
        await chargePayment(order);
        
        Sentry.captureMessage('Order processed successfully', {
            level: 'info',
            tags: { orderId },
        });
    } catch (error) {
        Sentry.captureException(error, {
            tags: { orderId },
            extra: { errorType: error.constructor.name },
        });
        throw error;
    } finally {
        transaction.finish();
    }
}

Best Practices

  1. Catch specific exceptions - Avoid catching generic Exception unless necessary
  2. Log at appropriate levels - DEBUG for detailed info, INFO for business events, WARN for recoverable issues, ERROR for failures
  3. Include context - Add correlation IDs, user IDs, request IDs to all logs
  4. Don't log sensitive data - Sanitize passwords, tokens, credit cards
  5. Use structured logging - JSON format for easier parsing and analysis
  6. Implement retries - For transient failures with exponential backoff
  7. Monitor errors - Use tools like Sentry, Application Insights, or CloudWatch
  8. Set up alerts - Notify team for critical errors or error rate spikes

Key Takeaways

  • Custom exception hierarchies improve error handling and API responses
  • Structured logging with correlation IDs enables effective troubleshooting
  • Retry patterns with exponential backoff handle transient failures
  • Production monitoring tools aggregate and alert on errors
  • Context-aware logging provides visibility into application behavior

Next Steps

  • Implement distributed tracing with OpenTelemetry
  • Set up log aggregation with ELK stack or Splunk
  • Create error budgets and SLOs for reliability
  • Add circuit breakers for failing services

Additional Resources


Log what matters. Handle what fails.