Rust allows you to propagate errors automatically with the ?
keyword, short-circuiting the method's execution and returning the error.
Previously, we attempted to modify the IL to propagate errors with Fody, turning this:
Into this:
It worked! Sort of, turns out C# is complex and trying to modify the IL will be rabbit hole we may never escape from. but is there a way we can have the compiler deal with all the complexity for us? A friend pointed out another even more bonkers idea.
await there one second...
The await
operator suspends execution of the enclosing function until the asynchronous operation is completed. To do this under the hood it generates a state machine. I'm not going to begin to try to explain this because 1) I don't know enough, 2) Stephen Toub exists.
What we will end up with is this:
The key to achieving this is that we can control how that state machine behaves by creating a custom AsyncMethodBuilder
, allowing us to short-circuit the method execution when it encounters an error result.
The full code:
When we await someResult
, the compiler generates code that calls GetAwaiter()
on the Result<T>
, returning our special ResultAwaiter<T>
.
In the ResultAwaiter<T>.GetResult()
method we check if there was an error:
So when we await a Result<T>
that is an error we set this error on the builder instead of throwing the exception directly.
Then we throw an exception! Wait, weren't we trying to get rid of exceptions... Whatever, it is a special exception! The state machine will then tell our a builder that an exception has happened and we store that exception plus a failed result.
Now how do we get all this out? Our builder has the Task
property which is used by the state machine to get the final value. We have special handling here to return an error result if there was a ResultException
thrown.
And that is basically what creates the short-circuit: when an error Result<T>
is awaited, the execution of the async method is just stopped at that point, with the method returning a failed Result<T>
containing the original exception instead of throwing an exception to the caller.
Running it
If we run this we should see this in the console:
Error: The input string 'b' was not in a correct format.
So it didn't reach the log saying the inputs were parsed. Now if we change our program to run:
Now we see:
Successfully parsed inputs
2
Wrapping up
Obviously, don't do this. It is wildly abusing await
and just plain madness! But it is fun to learn and I understand a little bit more about how async and await are achieved in C#.
Big thanks to Kostia for reading my previous post and pointing me in the direction of more crazy things to play with.
Here is a link to a repo with all the code: https://github.com/Hazzamanic/AwaitableResult.