Microsoft recommends against using exceptions for regular program flow, as they are slower than other control flow methods. Exceptions should be reserved for handling rare or unexpected situations, not the usual flow of the program. (Microsoft Docs: Best Practices for Exceptions - .NET | Microsoft Learn)

A more efficient and elegant alternative is to use a Result object, which effectively represents the outcome of an operation. This approach communicates the result clearly without relying on exceptions, offering simplicity and clarity.

Why use the Result object?

  • Improved expressiveness: The function signature clearly indicates possible outcomes, making the code more self-explanatory.
  • Better performance: This method avoids the performance overhead of throwing and catching exceptions, especially in scenarios where errors are anticipated.
  • Consistent error handling: Errors are turned into values that can be passed, modified, or logged easily, creating a more predictable and uniform error-handling process.
  • Customizable: The Result class can be easily extended to include additional information, validation errors, or other contextual data.

Example code

Exceptions style

public async Task<Project> GetProject(int projectKey)
{
    var project = await _projectRepository.Get(projectKey);

    if (project is null)
    {
       throw ProjectNotFoundException();
    }

    return project;
}

Result Style

public async Task<Result<Project>> GetProject(int projectKey)
{
    var project = await _projectRepository.Get(projectKey);

    if (project is null)
    {
       return ResultBuilder.WithNotFoundError<Project>(projectKey);
    }

    return ResultBuilder.WithSuccess<Project>(project);
}