Awesome
Relent
The Relent is a library that provides explicit error handling without relying on null or exceptions and resilience for uncertain operations in Unity/C#.
Features
- You don't need to rely on null and exceptions to handle errors.
- You can distinguish expected errors and unexpected (fatal) errors.
- You are forced to think handle failures.
- It makes the code a lot easier to read.
- You obtain resilience for uncertain operations e.g. HTTP communication.
How to import by Unity Package Manager
Add this dependency to your Packages/manifest.json
:
{
"dependencies": {
"com.mochineko.relent": "https://github.com/mochi-neko/Relent.git?path=/Assets/Mochineko/Relent#0.2.0",
"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask",
...
}
}
Modules
This library contains these modules:
Result
Result is a simple and explicit error handling module.
Core specification
Usage
Let MyOperation()
be your operation that can be failed
with any exception,
public MyObject MyOperation()
{
// do something that can throw exception
if (anyCondition)
{
return new MyObject();
}
else
{
throw new HandledException("Any handled exception");
}
}
you can wrap result by using IResult<TResult>
like this:
public IResult<MyObject> MyOperationByResult()
{
try
{
var myObject = MyOperation();
return ResultFactory.Succeed(myObject);
}
// Catch any exception what you want to handle.
catch (HandledException exception)
{
return ResultFactory.Fail<MyObject>(
$"Why did it fail? {exception.Message}");
}
catch (Exception exception)
{
// Panic! Unexpected exception what you don't want to handle.
throw;
}
}
then you can handle the result like this:
public void MyMethod()
{
IResult<MyObject> result = MyOperationByResult();
if (result is ISuccessResult<MyObject> successResult)
{
// do something with successResult.Result
}
else if (result is IFailureResult failureResult)
{
// do something with failureResult.Message
}
}
Once you have wrapped the result, you don't need to worry about null and exceptions expect for the fatal exception that you don't want to handle.
Of course, you can define your operation without null or any exception at first like this:
public IResult<MyObject> MyOperation()
{
if (anyCondition)
{
return ResultFactory.Succeed(myObject);
}
else
{
return ResultFactory.Fail<MyObject>(
"Why did it fail?");
}
}
Samples
Uncertain Result
UncertainResult is an explicit error handling module for an uncertain operation that can be retryable failure, e.g. HTTP communication for a WebAPI.
Core specification
Usage
Let MyOperation()
be your uncertain operation that can be failed
and be retryable with any exception,
public MyObject MyOperation()
{
// do something that can throw exception
if (anyCondition)
{
return new MyObject();
}
else if (anyOtherCondition)
{
throw new RetryableException("Any retryable exception");
}
else
{
throw new HandledException("Any exception");
}
}
you can wrap result by using IUncertainResult<TResult>
like this:
public IUncertainResult<MyObject> MyOperationByResult()
{
try
{
var myObject = MyOperation();
return ResultFactory.Succeed(myObject);
}
// Catch any exception what you want to handle as retryable.
catch (RetryableException exception)
{
return ResultFactory.Retry<MyObject>(
$"Why did it fail? {exception.Message}");
}
// Catch any exception what you want to handle as failure.
catch (HandledException exception)
{
return ResultFactory.Fail<MyObject>(
$"Why did it fail? {exception.Message}");
}
catch (Exception exception)
{
// Panic! Unexpected exception what you don't want to handle.
throw;
}
}
then you can handle the result like this:
public void MyMethod()
{
IUncertaionResult<MyObject> result = MyOperationByResult();
if (result is IUncertaionSuccessResult<MyObject> successResult)
{
// do something with successResult.Result
}
else if (result is IUncertaionRetryResult retryableResult)
{
// do something with retryResult.Message
// can retry operation
}
else if (result is IUncertaionFailureResult failureResult)
{
// do something with failureResult.Message
}
}
You can retry operation
when the result can cast IUncertainRetryResult
.
Also you can use switch
syntax like this:
public void MyMethod()
{
IUncertaionResult<MyObject> result = MyOperationByResult();
switch (result)
{
case IUncertaionSuccessResult<MyObject> successResult:
// do something with successResult.Result
break;
case IUncertaionRetryResult retryableResult:
// do something with retryResult.Message
// can retry operation
break;
case IUncertaionFailureResult failureResult:
// do something with failureResult.Message
break;
}
}
Once you have wrapped the result, you don't need to worry about null and exceptions expect for the fatal exception that you don't want to handle.
Of course, you can define your operation without null or any exception at first like this:
public IResult<MyObject> MyOperation()
{
if (anyCondition)
{
return ResultFactory.Succeed(myObject);
}
else if (anyOtherCondition)
{
return ResultFactory.Retry<MyObject>(
"Why did it fail?");
}
else
{
return ResultFactory.Fail<MyObject>(
"Why did it fail?");
}
}
Samples
Resilience
Resilience is a module that provides resilience for an uncertain operation caused by unpredictable factors, e.g. HTTP communication for a WebAPI.
It depends on UncertainResult.
Why don't I use Polly?
The timeout of Polly relies on OperationCanceledException
thrown by linked CancellationToken
(user cancellation token and timeout cancellation token)
in an operation.
When you use UncertainResult,
we want to catch OperationCanceledException
as retryable result,
then we cannot cancel by timeout.
Core specification
Features
Usage
Use some policies what you want to use.
Retry
The retry policy is a policy that retries an operation
when the operation returns an uncertain result
that can cast IUncertainRetryResult
.
See test codes.
Timeout
The timeout policy is a policy that cancels an operation when the operation takes too long.
See test codes.
Circuit Breaker
The circuit breaker policy is a policy
that breaks an operation
when the operation returns continuous uncertain results
that can cast IUncertainRetryResult
.
See test codes.
Bulkhead
The bulkhead policy is a policy that limits the number of operations that can be executed at the same time.
See test codes.
Wrap
The wrap policy is a policy that can combine some policies.
See test codes.
Samples
Extensions
- for Newtonsoft.Json
- for UniTask
Acknowledgments
This library is inspired by there posts and libraries:
- HttpClient - Error handling, a test driven approach
- Functional C#: Handling failures, input errors
- Polly
- Result in Rust
- anyhow::Result in Rust
Changelog
See CHANGELOG.
3rd Party Notices
See NOTICE.
License
Licensed under the MIT license.