To put it simply, C# has a very simple version of Haskell's do-notation, that (by virtue of a novel implementation method) only really works for Task types, but can be hacked to work with a Maybe or Either type. Apologies in advance: this is complicated, and I am not good enough at writing to make it any simpler.

It can be implemented with async and await. These are terrible keyword choices for this functionality. Both words reference the fact that a Task normally involves waiting, yet neither relates to or requires this behaviour at all.

async is a fascinating keyword. It has a huge amount of power over the execution of its body. This power is described in the appropriate AsyncMethodBuilder type, found by looking at the annotation on the function's return type. The builder type itself would ideally have a single interface to implement, but none exists; the required contract on the builder type can be found here. I've implemented that contract in a new type called ResultMethodBuilder<T>.

[AsyncMethodBuilder(typeof(ResultMethodBuilder<>))]
public abstract class BaseResult<T> : Result<T, BaseError>
{
  ...
}

BaseResult is now what the literature likes to call a 'Task-like' type. It's not — it has nothing at all in common with tasks (with one notable exception); but it's hardly fair to call them out on it when even the keywords are all about Tasks. Because it's Task-like, it can be the return type to an async function. There's one small (but actually quite large) limitation on this: your AsyncMethodBuilder can have at most one generic type parameters. To get around this, I've had to create a BaseError type and fix that as my error type (BaseError is implemented by BaseException (constructed with an Exception) or a BaseErrorMessage (constructed with a string)).

Because we've got a new abstract class to stand in for Result, we'll need to change our SuccessResult and FailureResult to subclass this new BaseResult.

Now that it's async (reminder: it isn't), it's time to make it awaitable (that is to say, something I can use with the await keyword: I'm not going to actually wait for it to complete). Given the complete absence of documentation for Task-like types, it is only expected that C# documented this bit very well. The new contract required on BaseResult is pretty simple.

[AsyncMethodBuilder(typeof(ResultMethodBuilder<>))]
public abstract class BaseResult<T> : IBaseResult<T, BaseError>
{
  ...

  public abstract IAwaitResult<T, BaseError> GetAwaiter();
}

The code for the AwaitResult is long but also simple.

public interface IAwaitResultErrorHolder<out TErr> : ICriticalNotifyCompletion
{
    TErr GetError();
}

public interface IAwaitResult<out T, out TErr> : IAwaitResultErrorHolder<out TErr>
{
    bool IsCompleted { get; }
    T GetResult();
}

public readonly struct SuccessAwaitResult<T, TErr> : IAwaitResult<T, TErr>
{
    private readonly T _item;
    public SuccessAwaitResult(T item) => _item = item;
    public void OnCompleted(Action continuation) => continuation();
    public void UnsafeOnCompleted(Action continuation) => continuation();
    public bool IsCompleted => true;
    public T GetResult() => _item;
    public TErr GetError() => throw new InvalidOperationException();
}

public readonly struct FailureAwaitResult<T, TErr> : IAwaitResult<T, TErr>
{
    private readonly TErr _err;
    public FailureAwaitResult(TErr err) => _err = err;

    public void OnCompleted(Action continuation)
    {
    }

    public void UnsafeOnCompleted(Action continuation)
    {
    }

    public bool IsCompleted => false;
    public T GetResult() => throw new InvalidOperationException();
    public TErr GetError() => _err;
}

The BaseResult types construct the relevant AwaitResult when GetAwaiter is called. The IAwaitResultErrorHolder will be important later for matching all AwaitResults using the is keyword (it's nearly impossible to determine its type without this).

Why on Earth am I doing this? There's an interesting line in the documentation:

"The purpose of the IsCompleted property is to determine if the task is already complete. If so, there is no need to suspend evaluation."

This is as close as the documentation will ever get to acknowledging that the await keyword has far more to do with success and failure than it has to do with waiting. Awaitable things are either complete or incomplete. By chance, C# also gives developers the opportunity to "wait" for things to be completed, hence the support for Tasks. Our Result types are also either successful or unsuccessful (with the added simplicity that we never need to wait), so we can reuse the pattern easily.

Now we have an awaitable asynchronous type (reminder: it's still neither) but we haven't yet described how C# should actually handle the various events that might occur in the function body. That's where the ResultMethodBuilder comes in. This single type handles every event in the async block.

public sealed class ResultMethodBuilder<T>
{

private T _result = default!;
private BaseError _err = default!;
private bool _hasResult;

public static ResultMethodBuilder<T> Create() =>
  new ResultMethodBuilder<T>();

public void Start<TStateMachine>(ref TStateMachine stateMachine)
  where TStateMachine : IAsyncStateMachine =>
    stateMachine.MoveNext();

public void SetStateMachine(IAsyncStateMachine stateMachine)
{
}

...

The instance is created using Create and started with Start — the logic here isn't relevant to me but is useful for types that require a bunch of setup before running the async block. Before the async block completes, it is guaranteed to have populated either the _result or _err variable and accordingly set _hasResult, so I am safe to use default! as an initial assignment.

...

public void SetException(Exception exception) =>
  _err = new BaseException(ExceptionDispatchInfo.Capture(exception));

public void SetResult(T result)
{
  _result = result;
  _hasResult = true;
}

...

If an exception is thrown during the evaluation of any expression, it's automatically caught and passed to SetException. The rest of the code in the block is then skipped. If all code in the block completes successfully, the contents of the return expression are passed to SetResult.

...

public void AwaitOnCompleted<TAwaiter, TStateMachine>(
  ref TAwaiter awaiter, ref TStateMachine stateMachine)
  where TAwaiter : INotifyCompletion
  where TStateMachine : IAsyncStateMachine
{
  if (awaiter is IAwaitResultErrorHolder<BaseError> baseErrorHolder)
  {
    _err = baseErrorHolder.GetError();
  }
  else
  {
    throw new InvalidAsynchronousStateException(
      "Unable to pull error information about error from underlying type - are you" +
      " awaiting another Type besides BaseResult?");
  }
}

public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(
  ref TAwaiter awaiter, ref TStateMachine stateMachine)
  where TAwaiter : ICriticalNotifyCompletion
  where TStateMachine : IAsyncStateMachine
{
  if (awaiter is IAwaitResultErrorHolder<BaseError> baseErrorHolder)
  {
    _err = baseErrorHolder.GetError();
  }
  else
  {
    throw new InvalidAsynchronousStateException(
      "Unable to pull error information about error from underlying type - are you" +
      " awaiting another Type besides BaseResult?");
  }
}

...

These functions are called when the awaited expression isn't (yet) complete. The TAwaiter type, whatever it is, implements INotifyCompletion, so we can pass an action to OnCompleted if we like. In the case of Task, this function will call this function with an action to advance the state of the passed in state machine, so that computation of the block continues once the awaited Task completes. This is why only awaited tasks block execution: AwaitOnCompleted isn't called for unawaited objects.

I did implement OnCompleted in my AwaitResult type, but I didn't need to because I don't need to ever call it: an incomplete BaseResult will remain so forever. I therefore don't need to advance the state machine; in doing so I end execution of the block at that statement automatically if it's incomplete. I then attempt a cast to an IAwaitResultErrorHolder so I can pull out and preserve the relevant error.

Why do I need the common interface IAwaitResultErrorHolder? C#'s type system, wonderful though it is, can't match IAwaitResult<T> for all T. Any function I would want to call on any IAwaitResult<T> must be a function indepedent of its generic type parameter so I can create an interface to capture the generic-type-agnostic function (in this case GetError()).

Why do we need to throw the exception when this type cast fails? C# doesn't actually complain if you decide to await a Task inside an async BaseResult<T> block, so we should crash if anyone tries: we've removed any logic to do with waiting so we can't properly await Tasks in these new async blocks. A more complete version of this code would persist all Task-specific functionality instead of throwing an exception.

...

public BaseResult<T> Task =>
  _hasResult
    ? new SuccessBaseResult<T>(_result)
    : (BaseResult<T>) new FailureBaseResult<T>(_err);

} // end of class

It doesn't really make sense to call this property Task (again, not necessarily related to Tasks themselves). It's called at the end of the function block to instantiate the object that will actually be returned from the async block. Note that we will either have an error saved (either from awaiting a Result in a failure state or a thrown exception) or a successful result (returned by the async block) so can construct a single BaseResult with all the information we need.

(An extra complication: note that if we happened to throw an InvalidAsynchronousStateException above, that exception would bubble outside this code block because it was thrown inside the builder type instead of in the code block managed by said builder type.)


With all of this code in place, async BaseResult blocks now have a huge amount of implicit control over the code within them. They automatically catch exceptions thrown in the block and let you craft a return object from them. They automatically end execution if an awaited BaseResult isn't in a success state so further steps are skipped. They automatically pull the error information from any failed awaits and preserve it in the block's returned BaseResult. They automatically unwrap successful values for use in the function, and automatically wrap those results back up after the block. We have essentially re-implemented bind, but more.

Let's tweak our Result-based dictionary lookup and list searching functions.

BaseResult<TValue> GetValue(this Dictionary<TKey, TValue> d, TKey key);
BaseResult<TValue> Find(this IList<TValue> l, Predicate<TValue> predicate);

Now let's define a basic block that looks up numbers in a dictionary and then adjusts the result.

private static readonly Dictionary<string, int> NumberWords =
  new Dictionary<string, int>
  {
    ["first"] = 1,
    ["second"] = 2,
    ["third"] = 3,
    ...
  };

public async BaseResult<int> GetArrayIndex(string indexAsWord)
{
  var index = await NumberWords.GetValue(indexAsWord);
  return index - 1;
}

Recall the messy function from the last post.

Result<ProcessStatus, LookupError> GetStatus(User user, Guid id) =>
  _usersToProcesses.GetValue(user)
    .Catch(_ => _logger.Info($"No processes for user '{user}'"))
    .Bind(processes => processes.Find(p => p.Id == id))
    .Catch(_ => _logger.Info(
      $"No process with id '{id}' found for user '{user}'")) 
    .Transform(p => p.Status);

What does that look like using BaseResult?

async BaseResult<ProcessStatus> GetStatus(User user, Guid id)
{
  var userProcesses = await _usersToProcesses.GetValue(user)
    .Catch(_ => _logger.Info($"No processes for user '{user}'"));

  var process = await userProcesses.Find(p => p.Id == id)
    .Catch(_ => _logger.Info(
      $"No process with id '{id}' found for user '{user}'"));
  
  return process.Status;
}

The only difference between these code blocks is that the second block won't log the second error if the first error has been logged, while the first one will. Hopefully it's now obvious how powerful these keywords are, and how inappropriate the choices for their names are: nothing here is async, yet I wouldn't consider this a gross abuse of the functionality either.

There are many questions to answer still. How do I return a failure result inside an async block, given that its return type is T and not BaseResult<T>? How can I attempt recovery if a lookup fails?

I'll figure out how to fix this later.