Skip to main content

Error Handling Pattern

Overview

This pattern defines comprehensive error handling strategies used across MepDash dashboards. Proper error handling ensures application resilience, provides meaningful feedback to users, and maintains system stability.

Core Concepts

  • Exception Handling: Catching and processing exceptions
  • Error Recovery: Attempting to recover from errors
  • Error Logging: Recording errors for debugging
  • User Notification: Informing users about issues
  • Graceful Degradation: Continuing operation despite errors

Pattern Implementation

Global Error Handler

Application-wide error handling:

// Pattern: Global Error Handler
public class GlobalErrorHandler
{
private readonly ILogger<GlobalErrorHandler> _logger;
private readonly INotificationService _notificationService;
private readonly IErrorReportingService _errorReporting;

public GlobalErrorHandler(
ILogger<GlobalErrorHandler> logger,
INotificationService notificationService,
IErrorReportingService errorReporting)
{
_logger = logger;
_notificationService = notificationService;
_errorReporting = errorReporting;

// Register global handlers
AppDomain.CurrentDomain.UnhandledException += OnUnhandledException;
TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
Application.Current.DispatcherUnhandledException += OnDispatcherUnhandledException;
}

private void OnUnhandledException(object sender, UnhandledExceptionEventArgs e)
{
var exception = e.ExceptionObject as Exception;
LogAndHandleError(exception, "Unhandled Exception", e.IsTerminating);
}

private void OnUnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
{
LogAndHandleError(e.Exception, "Unobserved Task Exception", false);
e.SetObserved(); // Prevent process termination
}

private void OnDispatcherUnhandledException(object sender,
DispatcherUnhandledExceptionEventArgs e)
{
LogAndHandleError(e.Exception, "Dispatcher Unhandled Exception", false);
e.Handled = true; // Prevent application crash
}

private void LogAndHandleError(Exception exception, string context, bool isFatal)
{
try
{
// Log the error
if (isFatal)
{
_logger.LogCritical(exception, "{Context}: Fatal error occurred", context);
}
else
{
_logger.LogError(exception, "{Context}: Error occurred", context);
}

// Report to error tracking service
_errorReporting.ReportError(exception, new ErrorContext
{
Context = context,
IsFatal = isFatal,
UserId = GetCurrentUserId(),
SessionId = GetSessionId(),
AdditionalData = CollectDiagnosticData()
});

// Notify user if appropriate
if (ShouldNotifyUser(exception))
{
var userMessage = CreateUserFriendlyMessage(exception);
_notificationService.ShowError(userMessage);
}

// Attempt recovery if not fatal
if (!isFatal)
{
AttemptRecovery(exception);
}
}
catch (Exception ex)
{
// Error handler failed - last resort logging
Debug.WriteLine($"Error handler failed: {ex}");
}
}
}

Structured Exception Handling

Consistent exception handling patterns:

// Pattern: Try-Catch-Finally with Logging
public class ServiceWithErrorHandling
{
private readonly ILogger<ServiceWithErrorHandling> _logger;

public async Task<Result<T>> ExecuteWithErrorHandlingAsync<T>(
Func<Task<T>> operation,
string operationName)
{
var stopwatch = Stopwatch.StartNew();

try
{
_logger.LogInformation("Starting operation: {OperationName}", operationName);

var result = await operation();

_logger.LogInformation("Operation completed successfully: {OperationName} in {ElapsedMs}ms",
operationName, stopwatch.ElapsedMilliseconds);

return Result<T>.Success(result);
}
catch (ValidationException vex)
{
_logger.LogWarning(vex, "Validation error in {OperationName}", operationName);
return Result<T>.ValidationError(vex.Message);
}
catch (BusinessException bex)
{
_logger.LogWarning(bex, "Business rule violation in {OperationName}", operationName);
return Result<T>.BusinessError(bex.Message);
}
catch (DataException dex)
{
_logger.LogError(dex, "Data error in {OperationName}", operationName);
return Result<T>.DataError("A data error occurred. Please try again.");
}
catch (TimeoutException tex)
{
_logger.LogError(tex, "Timeout in {OperationName} after {ElapsedMs}ms",
operationName, stopwatch.ElapsedMilliseconds);
return Result<T>.Timeout("The operation timed out. Please try again.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error in {OperationName}", operationName);
return Result<T>.SystemError("An unexpected error occurred.");
}
finally
{
stopwatch.Stop();
_logger.LogDebug("Operation {OperationName} took {ElapsedMs}ms",
operationName, stopwatch.ElapsedMilliseconds);
}
}
}

// Result wrapper
public class Result<T>
{
public bool IsSuccess { get; private set; }
public T Value { get; private set; }
public string ErrorMessage { get; private set; }
public ErrorType ErrorType { get; private set; }

public static Result<T> Success(T value) => new Result<T>
{
IsSuccess = true,
Value = value
};

public static Result<T> Error(string message, ErrorType type) => new Result<T>
{
IsSuccess = false,
ErrorMessage = message,
ErrorType = type
};
}

Retry Pattern

Automatic retry for transient errors:

// Pattern: Retry with Exponential Backoff
public class RetryPolicy
{
private readonly ILogger<RetryPolicy> _logger;
private readonly int _maxRetries;
private readonly TimeSpan _baseDelay;

public RetryPolicy(ILogger<RetryPolicy> logger, int maxRetries = 3, int baseDelayMs = 100)
{
_logger = logger;
_maxRetries = maxRetries;
_baseDelay = TimeSpan.FromMilliseconds(baseDelayMs);
}

public async Task<T> ExecuteAsync<T>(
Func<Task<T>> operation,
Func<Exception, bool> shouldRetry = null)
{
shouldRetry ??= IsTransientError;

for (int attempt = 0; attempt < _maxRetries; attempt++)
{
try
{
return await operation();
}
catch (Exception ex) when (attempt < _maxRetries - 1 && shouldRetry(ex))
{
var delay = TimeSpan.FromMilliseconds(
_baseDelay.TotalMilliseconds * Math.Pow(2, attempt));

_logger.LogWarning(ex,
"Attempt {Attempt}/{MaxRetries} failed. Retrying in {DelayMs}ms",
attempt + 1, _maxRetries, delay.TotalMilliseconds);

await Task.Delay(delay);
}
}

// Final attempt without catching
return await operation();
}

private bool IsTransientError(Exception ex)
{
return ex switch
{
TimeoutException _ => true,
HttpRequestException _ => true,
SqlException sqlEx => IsTransientSqlError(sqlEx),
IOException _ => true,
_ => false
};
}

private bool IsTransientSqlError(SqlException sqlEx)
{
// SQL Server transient error numbers
int[] transientErrors = { -2, 20, 64, 233, 10053, 10054, 10060, 40197, 40501, 40613, 49918, 49919, 49920 };
return transientErrors.Contains(sqlEx.Number);
}
}

Circuit Breaker Pattern

Preventing cascading failures:

// Pattern: Circuit Breaker
public class CircuitBreaker
{
private readonly ILogger<CircuitBreaker> _logger;
private readonly int _threshold;
private readonly TimeSpan _timeout;
private int _failureCount;
private DateTime _lastFailureTime;
private CircuitState _state = CircuitState.Closed;

public CircuitBreaker(ILogger<CircuitBreaker> logger, int threshold = 5, int timeoutSeconds = 60)
{
_logger = logger;
_threshold = threshold;
_timeout = TimeSpan.FromSeconds(timeoutSeconds);
}

public async Task<T> ExecuteAsync<T>(Func<Task<T>> operation)
{
if (_state == CircuitState.Open)
{
if (DateTime.UtcNow - _lastFailureTime < _timeout)
{
throw new CircuitOpenException("Circuit breaker is open");
}

_state = CircuitState.HalfOpen;
_logger.LogInformation("Circuit breaker transitioning to half-open");
}

try
{
var result = await operation();

if (_state == CircuitState.HalfOpen)
{
_state = CircuitState.Closed;
_failureCount = 0;
_logger.LogInformation("Circuit breaker closed");
}

return result;
}
catch (Exception ex)
{
RecordFailure();
throw;
}
}

private void RecordFailure()
{
_lastFailureTime = DateTime.UtcNow;
_failureCount++;

if (_failureCount >= _threshold)
{
_state = CircuitState.Open;
_logger.LogWarning("Circuit breaker opened after {FailureCount} failures", _failureCount);
}
}
}

public enum CircuitState
{
Closed,
Open,
HalfOpen
}

Custom Exceptions

Domain-specific exception types:

// Pattern: Custom Exception Hierarchy
public abstract class MepDashException : Exception
{
public string ErrorCode { get; }
public Dictionary<string, object> Context { get; }

protected MepDashException(string message, string errorCode = null, Exception inner = null)
: base(message, inner)
{
ErrorCode = errorCode ?? GetType().Name;
Context = new Dictionary<string, object>();
}

public MepDashException WithContext(string key, object value)
{
Context[key] = value;
return this;
}
}

public class ValidationException : MepDashException
{
public List<ValidationError> Errors { get; }

public ValidationException(string message, List<ValidationError> errors = null)
: base(message, "VALIDATION_ERROR")
{
Errors = errors ?? new List<ValidationError>();
}
}

public class BusinessException : MepDashException
{
public BusinessException(string message, string errorCode = "BUSINESS_ERROR")
: base(message, errorCode)
{
}
}

public class DataException : MepDashException
{
public DataException(string message, Exception inner = null)
: base(message, "DATA_ERROR", inner)
{
}
}

public class IntegrationException : MepDashException
{
public string System { get; }

public IntegrationException(string system, string message, Exception inner = null)
: base(message, $"INTEGRATION_{system}", inner)
{
System = system;
}
}

Error Recovery Strategies

Implementing recovery mechanisms:

// Pattern: Error Recovery
public class ErrorRecoveryService
{
private readonly ILogger<ErrorRecoveryService> _logger;
private readonly Dictionary<Type, IErrorRecoveryStrategy> _strategies;

public ErrorRecoveryService(ILogger<ErrorRecoveryService> logger)
{
_logger = logger;
_strategies = new Dictionary<Type, IErrorRecoveryStrategy>
{
[typeof(SessionExpiredException)] = new SessionRecoveryStrategy(),
[typeof(ConnectionLostException)] = new ConnectionRecoveryStrategy(),
[typeof(DataConflictException)] = new ConflictResolutionStrategy(),
[typeof(ResourceLockedException)] = new LockRecoveryStrategy()
};
}

public async Task<RecoveryResult> AttemptRecoveryAsync(Exception exception)
{
var exceptionType = exception.GetType();

if (_strategies.TryGetValue(exceptionType, out var strategy))
{
_logger.LogInformation("Attempting recovery for {ExceptionType}", exceptionType.Name);

try
{
var result = await strategy.RecoverAsync(exception);

if (result.Success)
{
_logger.LogInformation("Recovery successful for {ExceptionType}", exceptionType.Name);
}
else
{
_logger.LogWarning("Recovery failed for {ExceptionType}: {Reason}",
exceptionType.Name, result.FailureReason);
}

return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Recovery strategy failed for {ExceptionType}", exceptionType.Name);
return RecoveryResult.Failed("Recovery strategy threw exception");
}
}

_logger.LogDebug("No recovery strategy for {ExceptionType}", exceptionType.Name);
return RecoveryResult.NotAttempted();
}
}

public interface IErrorRecoveryStrategy
{
Task<RecoveryResult> RecoverAsync(Exception exception);
}

public class SessionRecoveryStrategy : IErrorRecoveryStrategy
{
public async Task<RecoveryResult> RecoverAsync(Exception exception)
{
// Attempt to reconnect session
try
{
await ReestablishSessionAsync();
return RecoveryResult.Successful();
}
catch
{
return RecoveryResult.Failed("Could not reestablish session");
}
}
}

User-Friendly Error Messages

Converting technical errors to user messages:

// Pattern: User Message Mapper
public class UserMessageMapper
{
private readonly Dictionary<Type, Func<Exception, string>> _mappings;

public UserMessageMapper()
{
_mappings = new Dictionary<Type, Func<Exception, string>>
{
[typeof(ValidationException)] = ex => ex.Message,
[typeof(BusinessException)] = ex => ex.Message,
[typeof(UnauthorizedAccessException)] = _ => "You don't have permission to perform this action.",
[typeof(TimeoutException)] = _ => "The operation timed out. Please try again.",
[typeof(SqlException)] = ex => MapSqlException((SqlException)ex),
[typeof(HttpRequestException)] = _ => "Network error. Please check your connection.",
[typeof(FileNotFoundException)] = ex => $"File not found: {((FileNotFoundException)ex).FileName}",
[typeof(OutOfMemoryException)] = _ => "The application is running low on memory. Please close other applications and try again."
};
}

public string GetUserMessage(Exception exception)
{
var exceptionType = exception.GetType();

// Check for exact match
if (_mappings.TryGetValue(exceptionType, out var mapper))
{
return mapper(exception);
}

// Check for base types
foreach (var mapping in _mappings)
{
if (mapping.Key.IsAssignableFrom(exceptionType))
{
return mapping.Value(exception);
}
}

// Default message
return "An unexpected error occurred. Please contact support if the problem persists.";
}

private string MapSqlException(SqlException sqlEx)
{
return sqlEx.Number switch
{
2627 => "This record already exists.",
547 => "This record cannot be deleted because it's being used elsewhere.",
2601 => "Duplicate entry. Please use a different value.",
-2 => "Database operation timed out. Please try again.",
_ => "A database error occurred. Please try again."
};
}
}

Dashboard-Specific Implementations

AR Payment Reversal

  • Queue processing error handling
  • Payment reversal rollback
  • Batch error recovery
  • See: AR Error Handling

Inventory Mini MRP

AP EFT Remittance

  • Email sending error recovery
  • Report generation error handling
  • Remittance processing rollback
  • See: AP Error Recovery

Best Practices

  1. Log all errors with appropriate context
  2. Use structured logging for better debugging
  3. Implement retry logic for transient failures
  4. Provide user-friendly messages for known errors
  5. Don't swallow exceptions silently
  6. Use circuit breakers for external dependencies
  7. Test error scenarios thoroughly
  8. Monitor error rates in production

Common Pitfalls

  1. Generic catch blocks - Catching Exception without handling specifics
  2. Silent failures - Swallowing exceptions without logging
  3. Information disclosure - Showing technical details to users
  4. No recovery - Not attempting to recover from errors
  5. Infinite retries - Retrying without limits

Examples