Files
thereadme/slides/i-like-having-options-for-control-flow/index.md
T

4.4 KiB

title, theme, layout, class, fonts
title theme layout class fonts
A Trick for Designing Friendly APIs in Go default center text-center
sans
CaskaydiaCove Nerd Font Mono

Why I love control flow in C#


layout: center class: text-center

The false dichotomy

Team "Never Throw"

"Exceptions are expensive! Treat errors as values! Not every failure is exceptional!"

Team "Always Throw"

"Results are noisy! Just throw and let the middleware handle it! Keep the code clean!"

> Why not both?

layout: center

The sweet spot

Global Handler
Prevent crashes
Controller
Matches result
Service Layer
Returns results
Deep / Domain
Throws exceptions

layout: center

An example in a web API


layout: center

Use exceptions for context agnostic code

public async Task<User?> GetUserAsync(int userId)
{
    // NOTE: EF Core might throw exceptions for connection issues, etc.
    var user = await _dbContext.Users.FindAsync(userId);

    // NOTE: Domain logic might throw exceptions for business rules
    if (user?.IsBanned)
    {
        throw new UserBannedException(userId);
    }

    return user;
}

Service layer returns results

public async Task<Result<User>> GetUserAsync(int userId)
{
    try
    {
        var user = await _userService.GetUserAsync(userId);

        if (user is null)
        {
            return Result.Failure<User>(new UserNotFoundException(userId));
        }

        return Result.Success(user);
    }
    catch (UserBannedException ex)
    {
        return Result.Failure<User>(ex);
    }
}

Match results in the controller

public static async Task<IResult> GetUserHandler(int userId, IUserService _service)
{
    var result = await _service.GetUserAsync(userId);

    return result.Match(
        user => Ok(user),
        error => error switch
        {
            UserNotFoundException => Results.NotFound(),
            UserBannedException => Results.Forbid(),
            _ => Results.InternalServerError()
        }
    );
}

Global handler for unexpected errors

public async ValueTask<bool> TryHandleAsync(
    HttpContext httpContext,
    Exception exception,
    CancellationToken cancellationToken
)
{
    _logger.LogError(
        exception,
        "Exception occurred: {Message}", exception.Message
    );

    httpContext.Response.StatusCode =
        StatusCodes.Status500InternalServerError;

    await httpContext.Response
        .WriteAsync("An unexpected error occurred.", cancellationToken);

    return true;
}

layout: center

C# gives me the freedom to choose

  • Use exceptions when deep in the call stack.
  • Use result types where business logic is centralized.
  • Use global handlers for unexpected exceptions and to prevent crashes.