Catch
The Catch monad represents operations that may throw exceptions, converting exception-based error handling into value-based handling. It captures exceptions as values, enabling functional composition without try-catch blocks.
Purpose
Section titled “Purpose”Catch provides a way to handle exceptions as values rather than control flow. This is particularly useful when working with third-party APIs or legacy code that uses exceptions, while keeping your application code functional and composable.
Producer Perspective
Section titled “Producer Perspective”Wrap exception-throwing code to return Catch:
// Simple Catchpublic class FileOperations{ public Catch DeleteFile(string path) { try { File.Delete(path); return Catch.Success(); } catch (Exception ex) { return Catch.Failed(ex); } }}
// Catch<TResult>public class ConfigurationService{ public Catch<AppConfig> LoadConfiguration(string filePath) { try { var json = File.ReadAllText(filePath); var config = JsonSerializer.Deserialize<AppConfig>(json);
// Implicit conversion from TResult to Catch<TResult> return config!; } catch (Exception ex) { // Implicit conversion from Exception to Catch<TResult> return ex; } }
public Catch<string> ReadConfigValue(string key) { try { var value = _configProvider.GetValue(key); return Catch<string>.Success(value); } catch (Exception ex) { return Catch<string>.Failed(ex); } }}
// Catch<TResult, TError>public record ConfigError(string Code, string Message, string? Details);
public class SecureConfigService{ public Catch<AppConfig, ConfigError> LoadSecureConfig(string filePath) { try { var config = LoadAndValidate(filePath); return Catch<AppConfig, ConfigError>.Success(config); } catch (FileNotFoundException ex) { return Catch<AppConfig, ConfigError>.Failed( new ConfigError("FILE_NOT_FOUND", "Configuration file not found", ex.Message)); } catch (JsonException ex) { return Catch<AppConfig, ConfigError>.Failed( new ConfigError("INVALID_JSON", "Invalid configuration format", ex.Message)); } catch (Exception ex) { // Can also pass the raw exception return Catch<AppConfig, ConfigError>.Failed(ex); } }}Consumer Perspective
Section titled “Consumer Perspective”Handle exceptions as values without try-catch blocks:
public class ConfigurationManager{ readonly ConfigurationService _configService; readonly SecureConfigService _secureConfigService;
public void LoadAndApplyConfig(string path) { // Catch<TResult> var configResult = _configService.LoadConfiguration(path);
if (configResult.IsSuccess && configResult.TryGetResult(out var config)) { Console.WriteLine($"Configuration loaded: {config.AppName}"); ApplyConfiguration(config); } else if (configResult.TryGetException(out var exception)) { Console.WriteLine($"Failed to load configuration: {exception.Message}"); LogException(exception); } }
public void LoadSecureConfiguration(string path) { // Catch<TResult, TError> var result = _secureConfigService.LoadSecureConfig(path);
if (result.TryGetResult(out var config)) { Console.WriteLine("Secure configuration loaded successfully"); ApplyConfiguration(config); } else if (result.TryGetError(out var error)) { Console.WriteLine($"Configuration error: [{error.Code}] {error.Message}"); if (error.Details is not null) { Console.WriteLine($"Details: {error.Details}"); } } }
public void SafelyReadConfig(string key) { var result = _configService.ReadConfigValue(key);
// Using TryGetResult/TryGetException var message = result.TryGetResult(out var value) ? $"Config value: {value}" : result.TryGetException(out var ex) ? $"Error: {ex.Message}" : "Unknown state";
Console.WriteLine(message); }
public string LoadConfigWithMatch(string path) { var result = _configService.LoadConfiguration(path);
// Using Match() for functional composition return result.Match( config => $"Loaded: {config.AppName} v{config.Version}", exception => $"Failed to load: {exception.Message}"); }
public void ProcessConfigWithMatch(string path) { var result = _secureConfigService.LoadSecureConfig(path);
// Switch() for side effects. Catch<T, TError> is a three-case union // (success, error, exception), so Switch takes three actions. result.Switch( config => { Console.WriteLine("Configuration loaded successfully"); ApplyConfiguration(config); }, error => { Console.WriteLine($"Configuration error: {error.Message}"); LogError(error); }, exception => { Console.WriteLine($"Unexpected failure: {exception.Message}"); }); }
public void HandleConfigWithPatternMatching(string path) { var result = _configService.LoadConfiguration(path);
// Pattern matching with switch expression var status = result switch { { IsSuccess: true } => $"Config loaded: {result.AsT0.AppName}", { IsSuccess: false } when result.AsT1 is FileNotFoundException => "Config file not found", { IsSuccess: false } when result.AsT1 is JsonException => "Invalid config format", { IsSuccess: false } => $"Error: {result.AsT1.Message}", _ => "Unknown state" };
Console.WriteLine(status); }}Key Points
Section titled “Key Points”- Use
Catchto convert exception-based APIs into value-based ones - Choose the appropriate variant:
Catch- Simple exception capture without return valueCatch<TResult>- Capture results or exceptionsCatch<TResult, TError>- Custom error representation for specific exception types
- Use
Match()for functional composition of potentially failing operations - Pattern matching with
switchexpressions enables exception-type-specific handling - Access specific exception types with pattern guards (e.g.,
when result.AsT1 is FileNotFoundException) - Implicit conversions from
TResultandExceptionsimplify creation IsSuccessindicates whether the operation succeededTryGetResultandTryGetExceptionprovide safe access to values- Particularly useful for wrapping file I/O, network calls, and third-party libraries
- Enables functional composition of potentially failing operations