Home

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

  1. You don't need to rely on null and exceptions to handle errors.
  2. You can distinguish expected errors and unexpected (fatal) errors.
  3. You are forced to think handle failures.
  4. It makes the code a lot easier to read.
  5. 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

Acknowledgments

This library is inspired by there posts and libraries:

Changelog

See CHANGELOG.

3rd Party Notices

See NOTICE.

License

Licensed under the MIT license.