🔷 Error Handling in C# TuskLang

C# Documentation

Error Handling in C# TuskLang

Overview

Error handling is critical for building robust and reliable TuskLang applications. This guide covers exception handling, custom exceptions, error responses, logging, and recovery strategies for C# applications.

🚨 Exception Handling

Global Exception Handler

public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;
    private readonly TSKConfig _config;
    
    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger, TSKConfig config)
    {
        _logger = logger;
        _config = config;
    }
    
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        var errorResponse = new ErrorResponse();
        var statusCode = HttpStatusCode.InternalServerError;
        
        switch (exception)
        {
            case ValidationException validationEx:
                errorResponse.Message = "Validation failed";
                errorResponse.Errors = new List<string> { validationEx.Message };
                statusCode = HttpStatusCode.BadRequest;
                break;
                
            case NotFoundException notFoundEx:
                errorResponse.Message = notFoundEx.Message;
                statusCode = HttpStatusCode.NotFound;
                break;
                
            case UnauthorizedException unauthorizedEx:
                errorResponse.Message = unauthorizedEx.Message;
                statusCode = HttpStatusCode.Unauthorized;
                break;
                
            case ForbiddenException forbiddenEx:
                errorResponse.Message = forbiddenEx.Message;
                statusCode = HttpStatusCode.Forbidden;
                break;
                
            case DatabaseException dbEx:
                errorResponse.Message = "Database operation failed";
                statusCode = HttpStatusCode.InternalServerError;
                _logger.LogError(dbEx, "Database error occurred");
                break;
                
            case ConfigurationException configEx:
                errorResponse.Message = "Configuration error";
                errorResponse.Errors = new List<string> { configEx.Message };
                statusCode = HttpStatusCode.InternalServerError;
                _logger.LogError(configEx, "Configuration error occurred");
                break;
                
            default:
                errorResponse.Message = "An unexpected error occurred";
                statusCode = HttpStatusCode.InternalServerError;
                _logger.LogError(exception, "Unhandled exception occurred");
                break;
        }
        
        // Add trace ID for debugging
        errorResponse.TraceId = httpContext.TraceIdentifier;
        
        // Include detailed error information in development
        if (_config.Get<string>("app.environment") == "development")
        {
            errorResponse.Details = new ErrorDetails
            {
                ExceptionType = exception.GetType().Name,
                StackTrace = exception.StackTrace,
                InnerException = exception.InnerException?.Message
            };
        }
        
        httpContext.Response.StatusCode = (int)statusCode;
        httpContext.Response.ContentType = "application/json";
        
        var json = JsonSerializer.Serialize(errorResponse, new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        });
        
        await httpContext.Response.WriteAsync(json, cancellationToken);
        
        return true;
    }
}

public class ErrorResponse { public string Message { get; set; } = string.Empty; public List<string> Errors { get; set; } = new(); public string? TraceId { get; set; } public ErrorDetails? Details { get; set; } }

public class ErrorDetails { public string? ExceptionType { get; set; } public string? StackTrace { get; set; } public string? InnerException { get; set; } }

Custom Exceptions

public class ValidationException : Exception
{
    public List<string> Errors { get; }
    
    public ValidationException(string message) : base(message)
    {
        Errors = new List<string> { message };
    }
    
    public ValidationException(List<string> errors) : base("Validation failed")
    {
        Errors = errors;
    }
}

public class NotFoundException : Exception { public string ResourceType { get; } public object ResourceId { get; } public NotFoundException(string resourceType, object resourceId) : base($"{resourceType} with id {resourceId} not found") { ResourceType = resourceType; ResourceId = resourceId; } public NotFoundException(string message) : base(message) { ResourceType = "Resource"; ResourceId = "unknown"; } }

public class UnauthorizedException : Exception { public UnauthorizedException(string message = "Unauthorized access") : base(message) { } }

public class ForbiddenException : Exception { public ForbiddenException(string message = "Access forbidden") : base(message) { } }

public class DatabaseException : Exception { public string Operation { get; } public string? SqlQuery { get; } public DatabaseException(string operation, string message, Exception? innerException = null) : base($"Database operation '{operation}' failed: {message}", innerException) { Operation = operation; } public DatabaseException(string operation, string message, string sqlQuery, Exception? innerException = null) : base($"Database operation '{operation}' failed: {message}", innerException) { Operation = operation; SqlQuery = sqlQuery; } }

public class ConfigurationException : Exception { public string ConfigurationKey { get; } public ConfigurationException(string configurationKey, string message) : base($"Configuration error for key '{configurationKey}': {message}") { ConfigurationKey = configurationKey; } }

public class TuskLangException : Exception { public string TuskLangCode { get; } public string? FilePath { get; } public TuskLangException(string tuskLangCode, string message, string? filePath = null) : base($"TuskLang error in code '{tuskLangCode}': {message}") { TuskLangCode = tuskLangCode; FilePath = filePath; } }

🔄 Retry Policies

Circuit Breaker Pattern

public class CircuitBreakerPolicy
{
    private readonly ILogger<CircuitBreakerPolicy> _logger;
    private readonly TSKConfig _config;
    private readonly Dictionary<string, CircuitBreakerState> _circuitBreakers = new();
    private readonly object _lock = new();
    
    public CircuitBreakerPolicy(ILogger<CircuitBreakerPolicy> logger, TSKConfig config)
    {
        _logger = logger;
        _config = config;
    }
    
    public async Task<T> ExecuteAsync<T>(string operationName, Func<Task<T>> operation)
    {
        var circuitBreaker = GetOrCreateCircuitBreaker(operationName);
        
        if (circuitBreaker.State == CircuitBreakerState.Open)
        {
            if (DateTime.UtcNow < circuitBreaker.OpenUntil)
            {
                throw new CircuitBreakerOpenException(operationName);
            }
            
            // Try to transition to half-open
            circuitBreaker.State = CircuitBreakerState.HalfOpen;
        }
        
        try
        {
            var result = await operation();
            
            // Success - reset failure count
            circuitBreaker.FailureCount = 0;
            circuitBreaker.State = CircuitBreakerState.Closed;
            
            return result;
        }
        catch (Exception ex)
        {
            circuitBreaker.FailureCount++;
            
            var failureThreshold = _config.Get<int>($"circuit_breaker.{operationName}.failure_threshold", 5);
            var openDuration = _config.Get<int>($"circuit_breaker.{operationName}.open_duration_seconds", 60);
            
            if (circuitBreaker.FailureCount >= failureThreshold)
            {
                circuitBreaker.State = CircuitBreakerState.Open;
                circuitBreaker.OpenUntil = DateTime.UtcNow.AddSeconds(openDuration);
                
                _logger.LogWarning("Circuit breaker opened for operation {OperationName} after {FailureCount} failures",
                    operationName, circuitBreaker.FailureCount);
            }
            
            throw;
        }
    }
    
    private CircuitBreakerState GetOrCreateCircuitBreaker(string operationName)
    {
        lock (_lock)
        {
            if (!_circuitBreakers.ContainsKey(operationName))
            {
                _circuitBreakers[operationName] = new CircuitBreakerState();
            }
            
            return _circuitBreakers[operationName];
        }
    }
}

public class CircuitBreakerState { public CircuitBreakerStateEnum State { get; set; } = CircuitBreakerStateEnum.Closed; public int FailureCount { get; set; } public DateTime OpenUntil { get; set; } }

public enum CircuitBreakerStateEnum { Closed, Open, HalfOpen }

public class CircuitBreakerOpenException : Exception { public string OperationName { get; } public CircuitBreakerOpenException(string operationName) : base($"Circuit breaker is open for operation '{operationName}'") { OperationName = operationName; } }

Retry Policy with Exponential Backoff

public class RetryPolicy
{
    private readonly ILogger<RetryPolicy> _logger;
    private readonly TSKConfig _config;
    
    public RetryPolicy(ILogger<RetryPolicy> logger, TSKConfig config)
    {
        _logger = logger;
        _config = config;
    }
    
    public async Task<T> ExecuteWithRetryAsync<T>(
        Func<Task<T>> operation,
        string operationName,
        int maxRetries = 3,
        TimeSpan? baseDelay = null)
    {
        var delay = baseDelay ?? TimeSpan.FromSeconds(1);
        var lastException = (Exception?)null;
        
        for (int attempt = 0; attempt <= maxRetries; attempt++)
        {
            try
            {
                return await operation();
            }
            catch (Exception ex) when (IsRetryableException(ex) && attempt < maxRetries)
            {
                lastException = ex;
                
                var waitTime = delay * Math.Pow(2, attempt); // Exponential backoff
                var jitter = Random.Shared.NextDouble()  0.1  waitTime.TotalMilliseconds; // Add jitter
                waitTime = waitTime.Add(TimeSpan.FromMilliseconds(jitter));
                
                _logger.LogWarning(ex, "Attempt {Attempt} failed for operation {OperationName}. Retrying in {WaitTime}ms",
                    attempt + 1, operationName, waitTime.TotalMilliseconds);
                
                await Task.Delay(waitTime);
            }
        }
        
        _logger.LogError(lastException, "Operation {OperationName} failed after {MaxRetries} retries",
            operationName, maxRetries);
        
        throw lastException!;
    }
    
    private bool IsRetryableException(Exception exception)
    {
        // Retry on transient exceptions
        return exception is TimeoutException ||
               exception is HttpRequestException ||
               exception is TaskCanceledException ||
               (exception is SqlException sqlEx && IsRetryableSqlException(sqlEx));
    }
    
    private bool IsRetryableSqlException(SqlException sqlException)
    {
        // SQL Server error codes that indicate transient failures
        var retryableErrorCodes = new[] { 2, 53, 64, 233, 10053, 10054, 10060, 40197, 40501, 40613 };
        return retryableErrorCodes.Contains(sqlException.Number);
    }
}

📝 Error Logging

Structured Error Logging

public class ErrorLogger
{
    private readonly ILogger<ErrorLogger> _logger;
    private readonly TSKConfig _config;
    
    public ErrorLogger(ILogger<ErrorLogger> logger, TSKConfig config)
    {
        _logger = logger;
        _config = config;
    }
    
    public void LogError(Exception exception, string operation, Dictionary<string, object>? context = null)
    {
        var logLevel = GetLogLevel(exception);
        
        var logData = new
        {
            Operation = operation,
            ExceptionType = exception.GetType().Name,
            Message = exception.Message,
            StackTrace = exception.StackTrace,
            InnerException = exception.InnerException?.Message,
            Context = context ?? new Dictionary<string, object>(),
            Timestamp = DateTime.UtcNow,
            Environment = _config.Get<string>("app.environment", "unknown")
        };
        
        _logger.Log(logLevel, exception, "Error in operation {Operation}: {Message}", operation, exception.Message);
        
        // Log additional context if available
        if (context != null && context.Any())
        {
            _logger.Log(logLevel, "Error context: {@Context}", context);
        }
    }
    
    public void LogValidationError(List<string> errors, string operation, Dictionary<string, object>? context = null)
    {
        var logData = new
        {
            Operation = operation,
            ErrorType = "Validation",
            Errors = errors,
            Context = context ?? new Dictionary<string, object>(),
            Timestamp = DateTime.UtcNow
        };
        
        _logger.LogWarning("Validation errors in operation {Operation}: {@Errors}", operation, errors);
        
        if (context != null && context.Any())
        {
            _logger.LogWarning("Validation context: {@Context}", context);
        }
    }
    
    public void LogConfigurationError(string configurationKey, string message, Exception? exception = null)
    {
        var logData = new
        {
            ConfigurationKey = configurationKey,
            Message = message,
            Exception = exception?.Message,
            Timestamp = DateTime.UtcNow
        };
        
        if (exception != null)
        {
            _logger.LogError(exception, "Configuration error for key {ConfigurationKey}: {Message}", 
                configurationKey, message);
        }
        else
        {
            _logger.LogError("Configuration error for key {ConfigurationKey}: {Message}", 
                configurationKey, message);
        }
    }
    
    private LogLevel GetLogLevel(Exception exception)
    {
        return exception switch
        {
            ValidationException => LogLevel.Warning,
            NotFoundException => LogLevel.Information,
            UnauthorizedException => LogLevel.Warning,
            ForbiddenException => LogLevel.Warning,
            DatabaseException => LogLevel.Error,
            ConfigurationException => LogLevel.Critical,
            _ => LogLevel.Error
        };
    }
}

🔧 Error Recovery

Graceful Degradation

public class GracefulDegradationService
{
    private readonly ILogger<GracefulDegradationService> _logger;
    private readonly TSKConfig _config;
    private readonly Dictionary<string, object> _fallbackValues = new();
    
    public GracefulDegradationService(ILogger<GracefulDegradationService> logger, TSKConfig config)
    {
        _logger = logger;
        _config = config;
        InitializeFallbackValues();
    }
    
    public async Task<T> ExecuteWithFallbackAsync<T>(
        Func<Task<T>> primaryOperation,
        Func<Task<T>> fallbackOperation,
        string operationName)
    {
        try
        {
            return await primaryOperation();
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Primary operation {OperationName} failed, using fallback", operationName);
            
            try
            {
                return await fallbackOperation();
            }
            catch (Exception fallbackEx)
            {
                _logger.LogError(fallbackEx, "Fallback operation {OperationName} also failed", operationName);
                throw;
            }
        }
    }
    
    public T GetConfigurationWithFallback<T>(string key, T defaultValue)
    {
        try
        {
            return _config.Get<T>(key, defaultValue);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to get configuration {Key}, using fallback value", key);
            
            if (_fallbackValues.TryGetValue(key, out var fallbackValue))
            {
                return (T)fallbackValue;
            }
            
            return defaultValue;
        }
    }
    
    public async Task<T> GetCachedValueWithFallbackAsync<T>(
        string cacheKey,
        Func<Task<T>> dataProvider,
        Func<Task<T>> fallbackProvider,
        TimeSpan cacheDuration)
    {
        try
        {
            // Try to get from cache first
            var cachedValue = await GetFromCacheAsync<T>(cacheKey);
            if (cachedValue != null)
            {
                return cachedValue;
            }
            
            // Try primary data provider
            var value = await dataProvider();
            await SetCacheAsync(cacheKey, value, cacheDuration);
            return value;
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Primary data provider failed for {CacheKey}, using fallback", cacheKey);
            
            try
            {
                var fallbackValue = await fallbackProvider();
                await SetCacheAsync(cacheKey, fallbackValue, cacheDuration);
                return fallbackValue;
            }
            catch (Exception fallbackEx)
            {
                _logger.LogError(fallbackEx, "Fallback data provider also failed for {CacheKey}", cacheKey);
                throw;
            }
        }
    }
    
    private void InitializeFallbackValues()
    {
        // Initialize fallback values for critical configuration
        _fallbackValues["database.connection_string"] = "Server=localhost;Database=fallback;";
        _fallbackValues["api.base_url"] = "https://fallback-api.example.com";
        _fallbackValues["cache.timeout"] = 30;
        _fallbackValues["logging.level"] = "Warning";
    }
    
    private async Task<T?> GetFromCacheAsync<T>(string key)
    {
        // Implementation depends on your caching mechanism
        await Task.CompletedTask;
        return default;
    }
    
    private async Task SetCacheAsync<T>(string key, T value, TimeSpan duration)
    {
        // Implementation depends on your caching mechanism
        await Task.CompletedTask;
    }
}

Health Checks

public class HealthCheckService : IHealthCheck
{
    private readonly IDbConnection _connection;
    private readonly TSKConfig _config;
    private readonly ILogger<HealthCheckService> _logger;
    
    public HealthCheckService(IDbConnection connection, TSKConfig config, ILogger<HealthCheckService> logger)
    {
        _connection = connection;
        _config = config;
        _logger = logger;
    }
    
    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
    {
        var checks = new List<HealthCheckResult>();
        
        // Database health check
        checks.Add(await CheckDatabaseHealthAsync());
        
        // Configuration health check
        checks.Add(await CheckConfigurationHealthAsync());
        
        // External service health check
        checks.Add(await CheckExternalServiceHealthAsync());
        
        var unhealthyChecks = checks.Where(c => c.Status == HealthStatus.Unhealthy).ToList();
        var degradedChecks = checks.Where(c => c.Status == HealthStatus.Degraded).ToList();
        
        if (unhealthyChecks.Any())
        {
            return HealthCheckResult.Unhealthy(
                "Health check failed",
                data: new Dictionary<string, object>
                {
                    ["unhealthy_checks"] = unhealthyChecks.Count,
                    ["degraded_checks"] = degradedChecks.Count
                });
        }
        
        if (degradedChecks.Any())
        {
            return HealthCheckResult.Degraded(
                "Some health checks are degraded",
                data: new Dictionary<string, object>
                {
                    ["degraded_checks"] = degradedChecks.Count
                });
        }
        
        return HealthCheckResult.Healthy("All health checks passed");
    }
    
    private async Task<HealthCheckResult> CheckDatabaseHealthAsync()
    {
        try
        {
            await _connection.OpenAsync();
            await _connection.ExecuteScalarAsync<int>("SELECT 1");
            await _connection.CloseAsync();
            
            return HealthCheckResult.Healthy("Database connection is healthy");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Database health check failed");
            return HealthCheckResult.Unhealthy("Database connection failed", ex);
        }
    }
    
    private async Task<HealthCheckResult> CheckConfigurationHealthAsync()
    {
        try
        {
            var requiredKeys = new[] { "database.connection_string", "api.base_url", "security.jwt_secret" };
            var missingKeys = new List<string>();
            
            foreach (var key in requiredKeys)
            {
                if (!_config.Has(key))
                {
                    missingKeys.Add(key);
                }
            }
            
            if (missingKeys.Any())
            {
                return HealthCheckResult.Unhealthy(
                    "Missing required configuration keys",
                    data: new Dictionary<string, object>
                    {
                        ["missing_keys"] = missingKeys
                    });
            }
            
            return HealthCheckResult.Healthy("Configuration is healthy");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Configuration health check failed");
            return HealthCheckResult.Unhealthy("Configuration check failed", ex);
        }
    }
    
    private async Task<HealthCheckResult> CheckExternalServiceHealthAsync()
    {
        try
        {
            var apiUrl = _config.Get<string>("api.base_url");
            if (string.IsNullOrEmpty(apiUrl))
            {
                return HealthCheckResult.Degraded("API URL not configured");
            }
            
            using var client = new HttpClient();
            client.Timeout = TimeSpan.FromSeconds(5);
            
            var response = await client.GetAsync($"{apiUrl}/health");
            
            if (response.IsSuccessStatusCode)
            {
                return HealthCheckResult.Healthy("External service is healthy");
            }
            
            return HealthCheckResult.Degraded(
                "External service returned non-success status",
                data: new Dictionary<string, object>
                {
                    ["status_code"] = (int)response.StatusCode
                });
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "External service health check failed");
            return HealthCheckResult.Degraded("External service is unavailable", ex);
        }
    }
}

📊 Error Monitoring

Error Metrics Collection

public class ErrorMetricsCollector
{
    private readonly ILogger<ErrorMetricsCollector> _logger;
    private readonly TSKConfig _config;
    private readonly Dictionary<string, ErrorMetrics> _metrics = new();
    private readonly object _lock = new();
    
    public ErrorMetricsCollector(ILogger<ErrorMetricsCollector> logger, TSKConfig config)
    {
        _logger = logger;
        _config = config;
    }
    
    public void RecordError(string operation, Exception exception)
    {
        lock (_lock)
        {
            if (!_metrics.ContainsKey(operation))
            {
                _metrics[operation] = new ErrorMetrics();
            }
            
            var metrics = _metrics[operation];
            metrics.TotalErrors++;
            metrics.LastErrorTime = DateTime.UtcNow;
            metrics.LastErrorMessage = exception.Message;
            
            var exceptionType = exception.GetType().Name;
            if (!metrics.ErrorTypes.ContainsKey(exceptionType))
            {
                metrics.ErrorTypes[exceptionType] = 0;
            }
            metrics.ErrorTypes[exceptionType]++;
        }
    }
    
    public void RecordSuccess(string operation)
    {
        lock (_lock)
        {
            if (!_metrics.ContainsKey(operation))
            {
                _metrics[operation] = new ErrorMetrics();
            }
            
            var metrics = _metrics[operation];
            metrics.TotalSuccesses++;
            metrics.LastSuccessTime = DateTime.UtcNow;
        }
    }
    
    public ErrorMetrics GetMetrics(string operation)
    {
        lock (_lock)
        {
            return _metrics.GetValueOrDefault(operation, new ErrorMetrics());
        }
    }
    
    public Dictionary<string, ErrorMetrics> GetAllMetrics()
    {
        lock (_lock)
        {
            return new Dictionary<string, ErrorMetrics>(_metrics);
        }
    }
    
    public async Task<ErrorReport> GenerateErrorReportAsync()
    {
        var allMetrics = GetAllMetrics();
        var report = new ErrorReport
        {
            GeneratedAt = DateTime.UtcNow,
            TotalOperations = allMetrics.Count,
            OperationsWithErrors = allMetrics.Count(kvp => kvp.Value.TotalErrors > 0),
            TotalErrors = allMetrics.Values.Sum(m => m.TotalErrors),
            TotalSuccesses = allMetrics.Values.Sum(m => m.TotalSuccesses),
            Operations = allMetrics.Select(kvp => new OperationMetrics
            {
                OperationName = kvp.Key,
                TotalErrors = kvp.Value.TotalErrors,
                TotalSuccesses = kvp.Value.TotalSuccesses,
                ErrorRate = kvp.Value.TotalErrors + kvp.Value.TotalSuccesses > 0 
                    ? (double)kvp.Value.TotalErrors / (kvp.Value.TotalErrors + kvp.Value.TotalSuccesses) 
                    : 0,
                LastErrorTime = kvp.Value.LastErrorTime,
                LastSuccessTime = kvp.Value.LastSuccessTime,
                ErrorTypes = new Dictionary<string, int>(kvp.Value.ErrorTypes)
            }).ToList()
        };
        
        return report;
    }
}

public class ErrorMetrics { public int TotalErrors { get; set; } public int TotalSuccesses { get; set; } public DateTime? LastErrorTime { get; set; } public DateTime? LastSuccessTime { get; set; } public string? LastErrorMessage { get; set; } public Dictionary<string, int> ErrorTypes { get; set; } = new(); }

public class ErrorReport { public DateTime GeneratedAt { get; set; } public int TotalOperations { get; set; } public int OperationsWithErrors { get; set; } public int TotalErrors { get; set; } public int TotalSuccesses { get; set; } public List<OperationMetrics> Operations { get; set; } = new(); }

public class OperationMetrics { public string OperationName { get; set; } = string.Empty; public int TotalErrors { get; set; } public int TotalSuccesses { get; set; } public double ErrorRate { get; set; } public DateTime? LastErrorTime { get; set; } public DateTime? LastSuccessTime { get; set; } public Dictionary<string, int> ErrorTypes { get; set; } = new(); }

📝 Summary

This guide covered comprehensive error handling strategies for C# TuskLang applications:

- Exception Handling: Global exception handler with custom exceptions and structured error responses - Retry Policies: Circuit breaker pattern and exponential backoff retry policies - Error Logging: Structured error logging with context and appropriate log levels - Error Recovery: Graceful degradation and health checks for system resilience - Error Monitoring: Error metrics collection and reporting for operational insights

These error handling strategies ensure your C# TuskLang applications are robust, resilient, and maintainable in production environments.