How to handle Task result in a more elegant way.

Serhii Korol - Jun 7 '23 - - Dev Community

I'm sure many developers use the "async/await" methods in their work. And sure, there is needed to handle errors. I'll explain with examples how to do it in the right way. And above all, let's create a simple console application:

public static class Program
{
    public static async Task Main()
    {
        var result = await MakeRequestGet();
        Console.WriteLine(result);
    }

    private static async Task<string> MakeRequestGet()
    {
        var client = new HttpClient();
        var result = await client.GetStringAsync("https://www.iana.org/domains/example");
        return result;
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, I call async HTTP request, and for this article, it'll return an error with a 403 code. As a rule, the code in this way nobody writes because you'll get an exception, and the application will be stopped. Usually, such calls wrap in try…catch block. And if you create a lot of similar methods, you should add to each method this block. I'm getting that this work makes Copilot, but, produces a lot of repeatable code. This code will work, but it's bad practice:

public static class Program
{
    public static async Task Main()
    {
        var result = await MakeRequestGet();
        Console.WriteLine(result);
    }

    private static async Task<string> MakeRequestGet()
    {
        try
        {
            var client = new HttpClient();
            var result = await client.GetStringAsync("https://www.iana.org/domains/example");
            return result;
        }
        catch (Exception e)
        {
            //you can use your favorite logger
                        Console.WriteLine(e);
            return string.Empty;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

I want to improve this code and make it more elegant and better. First of all, let's create our own result object that will keep results and errors. I created non-generic and generic classes.

public class Result
{
    protected readonly Exception? ExceptionError;

    public bool Success { get; }

    public string Message => ExceptionError?.Message ?? string.Empty;

    protected Result(bool success, Exception? exceptionError)
    {
        Success = success;
        ExceptionError = exceptionError;
    }

    public Exception GetError() => ExceptionError
        ?? throw new InvalidOperationException($"Error property for this Result not set.");

    public static Result Ok => new(true, null);

    public static Result Error(Exception error)
    {
        return new Result(false, error);
    }

    public static implicit operator Result(Exception exception) =>
        new(false, exception);
}

public sealed class Result<T> : Result
    where T : class
{
    private readonly T? _payload;

    private Result(T? payload, Exception? exceptionError, bool success) : base(success, exceptionError)
    {
        _payload = payload;
    }

    public Result(T payload) : base(true, null)
    {
        _payload = payload ?? throw new ArgumentNullException(nameof(payload));
    }

    private Result(Exception error) : base(false, error)
    {
    }

    public T GetOk() => Success
        ? _payload ?? throw new InvalidOperationException($"Payload for Result<{typeof(T)}> was not set.")
        : throw new InvalidOperationException($"Operation for Result<{typeof(T)}> was not successful.");

    public new Exception GetError() => ExceptionError
        ?? throw new InvalidOperationException($"Error property for Result<{typeof(T)}> not set.");

    public new static Result<T> Ok(T payload)
    {
        return new Result<T>(payload, null, true);
    }

    public new static Result<T> Error(Exception error)
    {
        return new Result<T>(null, error, false);
    }

    public static implicit operator Result<T>(T payload) =>
        new(payload, null, true);

    public static implicit operator Result<T>(Exception exception) =>
        new(exception);
}
Enter fullscreen mode Exit fullscreen mode

Next step, let's create a helper to handle the Task. There are also two versions of handlers - non-generic and generic.

public static class Helper
{

    public static async Task<Result> TryAwait(
        this Task task,
        Action<Exception> errorHandler = null
    ) {
        try {
            await task;
            return Result.Ok;
        }
        catch (Exception ex)
        {
            if (errorHandler is not null) errorHandler(ex);
            return ex;
        }
    }

    public static async Task<Result<T>> TryAwait<T>(
        this Task<T> task,
        Action<Exception> errorHandler = null
    ) where T : class {
        try {
            return await task;
        }
        catch (Exception ex)
        {
            if (errorHandler is not null) errorHandler(ex);
            return ex;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's check this out and rewrite our method:

    private static async Task<Result> MakeRequestGet()
    {
        var client = new HttpClient();
        var result = await client.GetStringAsync("https://www.iana.org/domains/example").TryAsync();

        return result;
    }
Enter fullscreen mode Exit fullscreen mode

And also make minor changes to the Main method:

    public static async Task Main()
    {
        var result = await MakeRequestGet();
        Console.WriteLine(result.Message);
    }
Enter fullscreen mode Exit fullscreen mode

Finally, you'll retrieve the same result as with the previous code, but without the try…catch block that you create only once. You can also add your favorite logger, but you need to make some changes in the code:

    private static async Task<Result> MakeRequestGet()
    {
        var client = new HttpClient();
        var result = await client.GetStringAsync("https://www.iana.org/domains/example").TryAsync(x =>
        {
            Console.WriteLine(x.Source);
        });
        return result;
    }
Enter fullscreen mode Exit fullscreen mode

As you can see, this code is more readable and cleaner. Also, you can change the Result class as you need.

As a bonus, I want to show how to handle void methods if you don't need callbacks. However, in the void methods also can be exceptions and you should handle it. Let's return to the Helper class and add another method. In this method, I get ILogger which handles exceptions.

    public static async Task TryAwait(
        this Task task,
        ILogger logger = null
    )
    {
        try
        {
            await task;
        }
        catch (Exception ex)
        {
            if(logger is not null) logger.Log(LogLevel.Error, ex.Message);
        }
    }
Enter fullscreen mode Exit fullscreen mode

We need to install the package and register it to use this extension. Let's add this code:

    private static ILogger<Program>? _logger;

    public static async Task Main()
    {
        SetLogger();
        await MakeRequestGet();
    }       

    private static void SetLogger()
    {
        var services = new ServiceCollection();
        services.AddLogging(builder =>
        {
            builder.AddConsole();
        });
        var serviceProvider = services.BuildServiceProvider();
        _logger = serviceProvider.GetService<ILogger<Program>>();
    }
Enter fullscreen mode Exit fullscreen mode

And also it requires making a few changes to this method:

    private static async Task MakeRequestGet()
    {
        var client = new HttpClient();
        await client.GetStringAsync("https://www.iana.org/domains/example").TryAwait(_logger);
    }
Enter fullscreen mode Exit fullscreen mode

As you can see, I invoke the logger into the TryAwait method. When there throws an exception, the logger shows your message. Sure, you can change it for your needing.

That's all. I hope for someone it'll be useful. Happy coding!

Buy Me A Beer

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .