🔷 Error Handling in C# TuskLang
Error Handling in C# TuskLang
Overview
Robust error handling is essential for building reliable and resilient applications. This guide covers exception handling, error recovery, fault tolerance, and error handling best practices for C# TuskLang applications.
🚨 Exception Handling
Global Exception Handler
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
private readonly TSKConfig _config;
private readonly IMetricsService _metricsService;
private readonly IAlertService _alertService;
public GlobalExceptionHandler(
ILogger<GlobalExceptionHandler> logger,
TSKConfig config,
IMetricsService metricsService,
IAlertService alertService)
{
_logger = logger;
_config = config;
_metricsService = metricsService;
_alertService = alertService;
}
public async Task<ExceptionHandlingResult> HandleAsync(Exception exception, ExceptionContext context)
{
try
{
// Log the exception
await LogExceptionAsync(exception, context);
// Record metrics
await RecordExceptionMetricsAsync(exception, context);
// Check if alert should be triggered
await CheckAndTriggerAlertAsync(exception, context);
// Determine handling strategy
var strategy = DetermineHandlingStrategy(exception, context);
// Execute handling strategy
var result = await ExecuteHandlingStrategyAsync(strategy, exception, context);
return result;
}
catch (Exception handlerException)
{
_logger.LogError(handlerException, "Exception handler failed while handling {ExceptionType}",
exception.GetType().Name);
return ExceptionHandlingResult.Failure("Exception handler failed", handlerException);
}
}
private async Task LogExceptionAsync(Exception exception, ExceptionContext context)
{
var logLevel = DetermineLogLevel(exception);
var message = FormatExceptionMessage(exception, context);
_logger.Log(logLevel, exception, message);
// Store exception in database for analysis
await StoreExceptionAsync(exception, context);
}
private LogLevel DetermineLogLevel(Exception exception)
{
return exception switch
{
ValidationException => LogLevel.Warning,
NotFoundException => LogLevel.Information,
UnauthorizedAccessException => LogLevel.Warning,
TimeoutException => LogLevel.Warning,
_ => LogLevel.Error
};
}
private string FormatExceptionMessage(Exception exception, ExceptionContext context)
{
var message = $"Exception in {context.OperationName}";
if (!string.IsNullOrEmpty(context.UserId))
{
message += $" for user {context.UserId}";
}
if (!string.IsNullOrEmpty(context.CorrelationId))
{
message += $" (Correlation: {context.CorrelationId})";
}
message += $": {exception.Message}";
return message;
}
private async Task StoreExceptionAsync(Exception exception, ExceptionContext context)
{
try
{
var exceptionRecord = new ExceptionRecord
{
Id = Guid.NewGuid(),
ExceptionType = exception.GetType().Name,
Message = exception.Message,
StackTrace = exception.StackTrace,
OperationName = context.OperationName,
UserId = context.UserId,
CorrelationId = context.CorrelationId,
Timestamp = DateTime.UtcNow,
Severity = DetermineSeverity(exception),
Context = JsonSerializer.Serialize(context.AdditionalContext)
};
// Store in database
await StoreExceptionRecordAsync(exceptionRecord);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to store exception record");
}
}
private ExceptionSeverity DetermineSeverity(Exception exception)
{
return exception switch
{
ValidationException => ExceptionSeverity.Low,
NotFoundException => ExceptionSeverity.Low,
UnauthorizedAccessException => ExceptionSeverity.Medium,
TimeoutException => ExceptionSeverity.Medium,
DatabaseException => ExceptionSeverity.High,
_ => ExceptionSeverity.High
};
}
private async Task RecordExceptionMetricsAsync(Exception exception, ExceptionContext context)
{
try
{
var tags = new Dictionary<string, object>
{
["exception_type"] = exception.GetType().Name,
["operation"] = context.OperationName,
["severity"] = DetermineSeverity(exception).ToString()
};
_metricsService.IncrementCounter("exceptions_total", 1, tags);
if (exception is TimeoutException)
{
_metricsService.IncrementCounter("timeouts_total", 1, tags);
}
else if (exception is DatabaseException)
{
_metricsService.IncrementCounter("database_errors_total", 1, tags);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to record exception metrics");
}
}
private async Task CheckAndTriggerAlertAsync(Exception exception, ExceptionContext context)
{
try
{
var severity = DetermineSeverity(exception);
if (severity >= ExceptionSeverity.High)
{
var alertMessage = $"High severity exception: {exception.GetType().Name} in {context.OperationName}";
await _alertService.TriggerAlertAsync(
"high_severity_exception",
alertMessage,
new Dictionary<string, object>
{
["exception_type"] = exception.GetType().Name,
["operation"] = context.OperationName,
["correlation_id"] = context.CorrelationId ?? "unknown"
});
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to trigger alert for exception");
}
}
private ExceptionHandlingStrategy DetermineHandlingStrategy(Exception exception, ExceptionContext context)
{
return exception switch
{
ValidationException => ExceptionHandlingStrategy.ReturnError,
NotFoundException => ExceptionHandlingStrategy.ReturnError,
UnauthorizedAccessException => ExceptionHandlingStrategy.ReturnError,
TimeoutException => ExceptionHandlingStrategy.Retry,
DatabaseException => ExceptionHandlingStrategy.Retry,
_ => ExceptionHandlingStrategy.ReturnError
};
}
private async Task<ExceptionHandlingResult> ExecuteHandlingStrategyAsync(
ExceptionHandlingStrategy strategy,
Exception exception,
ExceptionContext context)
{
return strategy switch
{
ExceptionHandlingStrategy.ReturnError => await HandleReturnErrorAsync(exception, context),
ExceptionHandlingStrategy.Retry => await HandleRetryAsync(exception, context),
ExceptionHandlingStrategy.CircuitBreaker => await HandleCircuitBreakerAsync(exception, context),
_ => ExceptionHandlingResult.Failure("Unknown handling strategy", exception)
};
}
private async Task<ExceptionHandlingResult> HandleReturnErrorAsync(Exception exception, ExceptionContext context)
{
var errorResponse = new ErrorResponse
{
ErrorCode = GetErrorCode(exception),
Message = GetUserFriendlyMessage(exception),
CorrelationId = context.CorrelationId,
Timestamp = DateTime.UtcNow
};
return ExceptionHandlingResult.Success(errorResponse);
}
private async Task<ExceptionHandlingResult> HandleRetryAsync(Exception exception, ExceptionContext context)
{
// This would typically involve retry logic
// For now, return error response
return await HandleReturnErrorAsync(exception, context);
}
private async Task<ExceptionHandlingResult> HandleCircuitBreakerAsync(Exception exception, ExceptionContext context)
{
// This would typically involve circuit breaker logic
// For now, return error response
return await HandleReturnErrorAsync(exception, context);
}
private string GetErrorCode(Exception exception)
{
return exception switch
{
ValidationException => "VALIDATION_ERROR",
NotFoundException => "NOT_FOUND",
UnauthorizedAccessException => "UNAUTHORIZED",
TimeoutException => "TIMEOUT",
DatabaseException => "DATABASE_ERROR",
_ => "INTERNAL_ERROR"
};
}
private string GetUserFriendlyMessage(Exception exception)
{
return exception switch
{
ValidationException => "The provided data is invalid. Please check your input and try again.",
NotFoundException => "The requested resource was not found.",
UnauthorizedAccessException => "You are not authorized to perform this action.",
TimeoutException => "The operation timed out. Please try again.",
DatabaseException => "A database error occurred. Please try again later.",
_ => "An unexpected error occurred. Please try again later."
};
}
private async Task StoreExceptionRecordAsync(ExceptionRecord record)
{
// Implementation would store in database
await Task.CompletedTask;
}
}public interface IExceptionHandler
{
Task<ExceptionHandlingResult> HandleAsync(Exception exception, ExceptionContext context);
}
public class ExceptionContext
{
public string OperationName { get; set; } = string.Empty;
public string? UserId { get; set; }
public string? CorrelationId { get; set; }
public Dictionary<string, object> AdditionalContext { get; set; } = new();
}
public class ExceptionHandlingResult
{
public bool Success { get; set; }
public object? Result { get; set; }
public string? ErrorMessage { get; set; }
public Exception? Exception { get; set; }
public static ExceptionHandlingResult Success(object? result = null)
{
return new ExceptionHandlingResult { Success = true, Result = result };
}
public static ExceptionHandlingResult Failure(string errorMessage, Exception? exception = null)
{
return new ExceptionHandlingResult
{
Success = false,
ErrorMessage = errorMessage,
Exception = exception
};
}
}
public enum ExceptionHandlingStrategy
{
ReturnError,
Retry,
CircuitBreaker
}
public enum ExceptionSeverity
{
Low,
Medium,
High,
Critical
}
public class ExceptionRecord
{
public Guid Id { get; set; }
public string ExceptionType { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public string? StackTrace { get; set; }
public string OperationName { get; set; } = string.Empty;
public string? UserId { get; set; }
public string? CorrelationId { get; set; }
public DateTime Timestamp { get; set; }
public ExceptionSeverity Severity { get; set; }
public string Context { get; set; } = string.Empty;
}
public class ErrorResponse
{
public string ErrorCode { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public string? CorrelationId { get; set; }
public DateTime Timestamp { get; set; }
}
Custom Exceptions
public class ValidationException : Exception
{
public List<ValidationError> Errors { get; }
public ValidationException(string message, List<ValidationError> errors) : base(message)
{
Errors = errors;
}
public ValidationException(string message) : base(message)
{
Errors = new List<ValidationError>();
}
}public class ValidationError
{
public string Field { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public string Code { get; set; } = string.Empty;
}
public class NotFoundException : Exception
{
public string ResourceType { get; }
public object ResourceId { get; }
public NotFoundException(string resourceType, object resourceId)
: base($"{resourceType} with id {resourceId} was not found")
{
ResourceType = resourceType;
ResourceId = resourceId;
}
}
public class DatabaseException : Exception
{
public string Operation { get; }
public string? Sql { get; }
public DatabaseException(string operation, string message, Exception? innerException = null)
: base(message, innerException)
{
Operation = operation;
}
public DatabaseException(string operation, string message, string sql, Exception? innerException = null)
: base(message, innerException)
{
Operation = operation;
Sql = sql;
}
}
public class ConfigurationException : Exception
{
public string ConfigurationKey { get; }
public ConfigurationException(string configurationKey, string message)
: base(message)
{
ConfigurationKey = configurationKey;
}
}
public class ServiceUnavailableException : Exception
{
public string ServiceName { get; }
public TimeSpan RetryAfter { get; }
public ServiceUnavailableException(string serviceName, TimeSpan retryAfter)
: base($"Service {serviceName} is currently unavailable. Retry after {retryAfter.TotalSeconds} seconds")
{
ServiceName = serviceName;
RetryAfter = retryAfter;
}
}
🔄 Error Recovery
Retry Policy
public class RetryPolicy
{
private readonly ILogger<RetryPolicy> _logger;
private readonly TSKConfig _config;
private readonly IMetricsService _metricsService;
public RetryPolicy(ILogger<RetryPolicy> logger, TSKConfig config, IMetricsService metricsService)
{
_logger = logger;
_config = config;
_metricsService = metricsService;
}
public async Task<T> ExecuteAsync<T>(
Func<Task<T>> operation,
RetryOptions options,
CancellationToken cancellationToken = default)
{
var attempt = 0;
var lastException = (Exception?)null;
while (attempt < options.MaxRetries)
{
try
{
attempt++;
_logger.LogDebug("Executing operation (attempt {Attempt}/{MaxRetries})",
attempt, options.MaxRetries);
var result = await operation();
if (attempt > 1)
{
_logger.LogInformation("Operation succeeded on attempt {Attempt}", attempt);
_metricsService.IncrementCounter("retry_success", 1, new Dictionary<string, object>
{
["attempt"] = attempt
});
}
return result;
}
catch (Exception ex) when (ShouldRetry(ex, options) && attempt < options.MaxRetries)
{
lastException = ex;
var delay = CalculateDelay(attempt, options);
_logger.LogWarning(ex, "Operation failed on attempt {Attempt}/{MaxRetries}. Retrying in {Delay}ms",
attempt, options.MaxRetries, delay.TotalMilliseconds);
_metricsService.IncrementCounter("retry_attempt", 1, new Dictionary<string, object>
{
["attempt"] = attempt,
["exception_type"] = ex.GetType().Name
});
await Task.Delay(delay, cancellationToken);
}
}
_logger.LogError(lastException, "Operation failed after {MaxRetries} attempts", options.MaxRetries);
_metricsService.IncrementCounter("retry_failure", 1);
throw lastException!;
}
private bool ShouldRetry(Exception exception, RetryOptions options)
{
if (options.RetryableExceptions.Contains(exception.GetType()))
{
return true;
}
if (exception is HttpRequestException httpEx)
{
return options.RetryableHttpStatusCodes.Contains(httpEx.StatusCode);
}
return false;
}
private TimeSpan CalculateDelay(int attempt, RetryOptions options)
{
return options.BackoffStrategy switch
{
BackoffStrategy.Fixed => options.BaseDelay,
BackoffStrategy.Exponential => TimeSpan.FromMilliseconds(
options.BaseDelay.TotalMilliseconds * Math.Pow(2, attempt - 1)),
BackoffStrategy.Linear => TimeSpan.FromMilliseconds(
options.BaseDelay.TotalMilliseconds * attempt),
_ => options.BaseDelay
};
}
}public class RetryOptions
{
public int MaxRetries { get; set; } = 3;
public TimeSpan BaseDelay { get; set; } = TimeSpan.FromSeconds(1);
public BackoffStrategy BackoffStrategy { get; set; } = BackoffStrategy.Exponential;
public List<Type> RetryableExceptions { get; set; } = new()
{
typeof(TimeoutException),
typeof(HttpRequestException),
typeof(DatabaseException)
};
public List<HttpStatusCode> RetryableHttpStatusCodes { get; set; } = new()
{
HttpStatusCode.RequestTimeout,
HttpStatusCode.InternalServerError,
HttpStatusCode.BadGateway,
HttpStatusCode.ServiceUnavailable,
HttpStatusCode.GatewayTimeout
};
}
public enum BackoffStrategy
{
Fixed,
Exponential,
Linear
}
Circuit Breaker
public class CircuitBreaker
{
private readonly ILogger<CircuitBreaker> _logger;
private readonly TSKConfig _config;
private readonly IMetricsService _metricsService;
private readonly string _name;
private readonly object _lock = new();
private CircuitBreakerState _state = CircuitBreakerState.Closed;
private int _failureCount = 0;
private DateTime _lastFailureTime = DateTime.MinValue;
private DateTime _lastStateChangeTime = DateTime.UtcNow;
public CircuitBreaker(
ILogger<CircuitBreaker> logger,
TSKConfig config,
IMetricsService metricsService,
string name)
{
_logger = logger;
_config = config;
_metricsService = metricsService;
_name = name;
}
public async Task<T> ExecuteAsync<T>(
Func<Task<T>> operation,
CancellationToken cancellationToken = default)
{
if (!await CanExecuteAsync())
{
throw new CircuitBreakerOpenException(_name);
}
try
{
var result = await operation();
await OnSuccessAsync();
return result;
}
catch (Exception ex)
{
await OnFailureAsync(ex);
throw;
}
}
private async Task<bool> CanExecuteAsync()
{
lock (_lock)
{
switch (_state)
{
case CircuitBreakerState.Closed:
return true;
case CircuitBreakerState.Open:
var timeout = TimeSpan.FromSeconds(_config.Get<int>($"circuit_breaker.{_name}.timeout_seconds", 60));
if (DateTime.UtcNow - _lastFailureTime >= timeout)
{
_state = CircuitBreakerState.HalfOpen;
_lastStateChangeTime = DateTime.UtcNow;
_logger.LogInformation("Circuit breaker {Name} moved to half-open state", _name);
}
return false;
case CircuitBreakerState.HalfOpen:
return true;
default:
return false;
}
}
}
private async Task OnSuccessAsync()
{
lock (_lock)
{
if (_state == CircuitBreakerState.HalfOpen)
{
_state = CircuitBreakerState.Closed;
_failureCount = 0;
_lastStateChangeTime = DateTime.UtcNow;
_logger.LogInformation("Circuit breaker {Name} moved to closed state", _name);
}
}
_metricsService.IncrementCounter("circuit_breaker_success", 1, new Dictionary<string, object>
{
["circuit_breaker"] = _name
});
}
private async Task OnFailureAsync(Exception exception)
{
lock (_lock)
{
_failureCount++;
_lastFailureTime = DateTime.UtcNow;
var failureThreshold = _config.Get<int>($"circuit_breaker.{_name}.failure_threshold", 5);
if (_state == CircuitBreakerState.Closed && _failureCount >= failureThreshold)
{
_state = CircuitBreakerState.Open;
_lastStateChangeTime = DateTime.UtcNow;
_logger.LogWarning("Circuit breaker {Name} moved to open state after {FailureCount} failures",
_name, _failureCount);
}
else if (_state == CircuitBreakerState.HalfOpen)
{
_state = CircuitBreakerState.Open;
_lastStateChangeTime = DateTime.UtcNow;
_logger.LogWarning("Circuit breaker {Name} moved back to open state", _name);
}
}
_metricsService.IncrementCounter("circuit_breaker_failure", 1, new Dictionary<string, object>
{
["circuit_breaker"] = _name,
["exception_type"] = exception.GetType().Name
});
}
public CircuitBreakerStatus GetStatus()
{
lock (_lock)
{
return new CircuitBreakerStatus
{
Name = _name,
State = _state,
FailureCount = _failureCount,
LastFailureTime = _lastFailureTime,
LastStateChangeTime = _lastStateChangeTime
};
}
}
public void Reset()
{
lock (_lock)
{
_state = CircuitBreakerState.Closed;
_failureCount = 0;
_lastStateChangeTime = DateTime.UtcNow;
_logger.LogInformation("Circuit breaker {Name} manually reset", _name);
}
}
}public enum CircuitBreakerState
{
Closed,
Open,
HalfOpen
}
public class CircuitBreakerStatus
{
public string Name { get; set; } = string.Empty;
public CircuitBreakerState State { get; set; }
public int FailureCount { get; set; }
public DateTime LastFailureTime { get; set; }
public DateTime LastStateChangeTime { get; set; }
}
public class CircuitBreakerOpenException : Exception
{
public string CircuitBreakerName { get; }
public CircuitBreakerOpenException(string circuitBreakerName)
: base($"Circuit breaker '{circuitBreakerName}' is open")
{
CircuitBreakerName = circuitBreakerName;
}
}
🛡️ Fault Tolerance
Fault Tolerant Service
public class FaultTolerantService
{
private readonly ILogger<FaultTolerantService> _logger;
private readonly TSKConfig _config;
private readonly RetryPolicy _retryPolicy;
private readonly CircuitBreaker _circuitBreaker;
private readonly IExceptionHandler _exceptionHandler;
public FaultTolerantService(
ILogger<FaultTolerantService> logger,
TSKConfig config,
RetryPolicy retryPolicy,
CircuitBreaker circuitBreaker,
IExceptionHandler exceptionHandler)
{
_logger = logger;
_config = config;
_retryPolicy = retryPolicy;
_circuitBreaker = circuitBreaker;
_exceptionHandler = exceptionHandler;
}
public async Task<T> ExecuteWithFaultToleranceAsync<T>(
Func<Task<T>> operation,
string operationName,
FaultToleranceOptions options,
CancellationToken cancellationToken = default)
{
var context = new ExceptionContext
{
OperationName = operationName,
CorrelationId = Guid.NewGuid().ToString()
};
try
{
// Execute with circuit breaker
var result = await _circuitBreaker.ExecuteAsync(async () =>
{
// Execute with retry policy
return await _retryPolicy.ExecuteAsync(operation, options.RetryOptions, cancellationToken);
}, cancellationToken);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Operation {OperationName} failed with fault tolerance", operationName);
// Handle exception
var handlingResult = await _exceptionHandler.HandleAsync(ex, context);
if (handlingResult.Success && handlingResult.Result is T result)
{
return result;
}
throw;
}
}
public async Task<T> ExecuteWithFallbackAsync<T>(
Func<Task<T>> primaryOperation,
Func<Task<T>> fallbackOperation,
string operationName,
FaultToleranceOptions options,
CancellationToken cancellationToken = default)
{
try
{
return await ExecuteWithFaultToleranceAsync(primaryOperation, operationName, options, cancellationToken);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Primary operation {OperationName} failed, trying fallback", operationName);
try
{
return await ExecuteWithFaultToleranceAsync(fallbackOperation, $"{operationName}_fallback", options, cancellationToken);
}
catch (Exception fallbackEx)
{
_logger.LogError(fallbackEx, "Fallback operation {OperationName} also failed", operationName);
throw;
}
}
}
public async Task<T> ExecuteWithTimeoutAsync<T>(
Func<Task<T>> operation,
TimeSpan timeout,
string operationName,
CancellationToken cancellationToken = default)
{
using var timeoutCts = new CancellationTokenSource(timeout);
using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(
timeoutCts.Token, cancellationToken);
try
{
return await operation().WaitAsync(combinedCts.Token);
}
catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested)
{
var timeoutException = new TimeoutException($"Operation {operationName} timed out after {timeout.TotalSeconds} seconds");
var context = new ExceptionContext
{
OperationName = operationName,
CorrelationId = Guid.NewGuid().ToString()
};
var handlingResult = await _exceptionHandler.HandleAsync(timeoutException, context);
if (handlingResult.Success && handlingResult.Result is T result)
{
return result;
}
throw timeoutException;
}
}
}public class FaultToleranceOptions
{
public RetryOptions RetryOptions { get; set; } = new();
public TimeSpan? Timeout { get; set; }
public bool EnableCircuitBreaker { get; set; } = true;
public bool EnableFallback { get; set; } = false;
}
📝 Summary
This guide covered comprehensive error handling strategies for C# TuskLang applications:
- Exception Handling: Global exception handler with logging, metrics, and alerting - Custom Exceptions: Domain-specific exceptions for better error categorization - Error Recovery: Retry policies and circuit breakers for fault tolerance - Fault Tolerance: Comprehensive fault tolerance patterns with fallbacks and timeouts
These error handling strategies ensure your C# TuskLang applications are resilient, reliable, and provide excellent user experience even when errors occur.