Rust-style error handling for C#
using OperationResult; using static OperationResult.Helpers; public Result<double, string> SqrtOperation(double argument) { if (argument < 0) { return Error("Argument must be greater than zero"); } double result = Math.Sqrt(argument); return Ok(result); } public void Method() { var result = SqrtOperation(123); if (result) { Console.WriteLine("Value is: {0}", result.Value); } else { Console.WriteLine("Error is: {0}", result.Error); } }-
Ok<>()Error<>()Ok<TResult>(TResult result)Error<TError>(TError error)
Result of some method when there is no TError type defined
public struct Result<TResult> { public readonly TResult Value; public bool IsError { get; } public bool IsSuccess { get; } public static implicit operator bool(Result<TResult> result); public static implicit operator Result<TResult>(TResult result); }Example
public Result<uint> Square(uint argument) { if (argument >= UInt16.MaxValue) { return Error(); } return Ok(argument * argument); }Either Result of some method or Error from this method
public struct Result<TResult, TError> { public readonly TError Error; public readonly TResult Value; public bool IsError { get; } public bool IsSuccess { get; } public void Deconstruct(out TResult result, out TError error); public static implicit operator bool(Result<TResult, TError> result); public static implicit operator Result<TResult, TError>(TResult result); }Also Result has shorthand implicit conversion from TResult type
using OperationResult; using static OperationResult.Helpers; public async Task<Result<string, HttpStatusCode>> DownloadPage(string url) { using (var client = new HttpClient()) using (var response = await client.GetAsync(url)) { if (response.IsSuccessStatusCode) { // return string as Result return await response.Content.ReadAsStringAsync(); } return Error(response.StatusCode); } }Either Result of some method or multiple Errors from this method
public struct Result<TResult, TError1, TError2, ...> { public readonly TError Error; public readonly TResult Value; public bool IsError { get; } public bool IsSuccess { get; } public void Deconstruct(out TResult result, out object error); public static implicit operator bool(Result<TResult, TError1, ...> result); public static implicit operator Result<TResult, TError1, ...>(TResult result); }Example
public Result<int, InnerError> Inner() { return Error(new InnerError()); } public Result<int, OuterError, InnerError> Outer() { var result = Inner(); if (!result) { return Error(result.Error); } return Error(new OuterError()); } public void Method() { var result = Outer(); if (result) { // ... } else if (result.HasError<InnerError>()) { Console.WriteLine("{0}", result.GetError<InnerError>()); } else if (result.HasError<OuterError>()) { Console.WriteLine("{0}", result.GetError<OuterError>()); } }Status of some operation without result when there is no TError type defined
public struct Status { public bool IsError { get; } public bool IsSuccess { get; } public static implicit operator bool(Status status); }Example
public Status IsOdd(int value) { if (value % 2 == 1) { return Ok(); } return Error(); }Status of some operation without result
public struct Status<TError> { public readonly TError Error; public bool IsError { get; } public bool IsSuccess { get; } public static implicit operator bool(Status<TError> status); }Example
public Status<string> Validate(string input) { if (String.IsNullOrEmpty(input)) { return Error("Input is empty"); } if (input.Length > 100) { return Error("Input is too long"); } return Ok(); }Status of some operation without result but with multiple Errors from this method
public struct Status<TError1, TError2, ...> { public readonly object Error; public bool IsError { get; } public bool IsSuccess { get; } public TError GetError<TError>(); public bool HasError<TError>(); public static implicit operator bool(Status<TError1, ...> status); }Example
public Status<InnerError> Inner() { return Error(new InnerError()); } public Status<OuterError, InnerError> Outer() { var result = Inner(); if (!result) { return Error(result.Error); } return Error(new OuterError()); }public static class Helpers { public static SuccessTag Ok(); public static ErrorTag Error(); public static SuccessTag<TResult> Ok<TResult>(TResult result); public static ErrorTag<TError> Error<TError>(TError error); }A performance comparsion with other error handling techniques
| Method | SuccessRate | Mean | StdDev | Gen 0 | Allocated |
|---|---|---|---|---|---|
TResult Operation() + Exception | 50 | 1 068 491 ns | 2 754.82 ns | - | 10.4 kB |
Result<TResult, TError> Operation() | 50 | 2 025 ns | 0.67 ns | - | 0 B |
Tuple<TResult, TError> Operation() | 50 | 957 ns | 1.71 ns | 0.9669 | 1.6 kB |
bool Operation(out TResult value, out TError error) | 50 | 650 ns | 0.15 ns | - | 0 B |
TResult Operation() + Exception | 90 | 212 520 ns | 529.55 ns | - | 2.08 kB |
Result<TResult, TError> Operation() | 90 | 1 995 ns | 1.86 ns | - | 0 B |
Tuple<TResult, TError> Operation() | 90 | 815 ns | 1.18 ns | 0.9669 | 1.6 kB |
bool Operation(out TResult value, out TError error) | 90 | 463 ns | 0.41 ns | - | 0 B |
TResult Operation() + Exception | 99 | 22 069 ns | 52.44 ns | - | 208 B |
Result<TResult, TError> Operation() | 99 | 1 989 ns | 2.84 ns | - | 0 B |
Tuple<TResult, TError> Operation() | 99 | 778 ns | 1.31 ns | 0.9669 | 1.6 kB |
bool Operation(out TResult value, out TError error) | 99 | 430 ns | 0.34 ns | - | 0 B |