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 |