The Result Pattern: A Smarter Way to Handle Failures

The Result Pattern A Smarter Way to Handle Failures

Exception handling is everywhere, and often, it’s a mess.

Nested try-catch blocks, unreadable logs, no status code return and unclear return paths make debugging a painful experience. But what if there was a simpler, cleaner, and more predictable way to deal with failures?

The Result Pattern offers just that. Instead of relying on exceptions for flow control, it embraces explicit success and failure outcomes, making your code easier to read, test, and maintain.

In this post, I’ll show you why the Result Pattern might be the smarter alternative your codebase needs.

This Could Ruin Your Code

Let’s say you’re building an API that fetches a resource by ID. A common mistake is treating a missing record as an exception scenario:

C#
				public async Task<NoticeResponse> GetNoticeByIdAsync(int id)
{
	var notice = await noticeRepository.GetNoticeByIdAsync(id);

	if (notice is null)
	{
		throw new KeyNotFoundException("Notice not found"); //Custom
	}

	return notice.MapToResponse();
}
			

This approach has several issues:

  • You’re throwing an exception for a known and expected scenario.
  • The caller must know which exceptions to handle, and this isn’t obvious from the method signature.
  • Consumers must rely on try-catch to handle what should be a normal control flow.

 

Exceptions shine when you’re dealing with unpredictable problems you can’t anticipate. Ideally, they should be caught and managed at the lowest possible level in your application.

But when you do know, how to handle a failure case?

That’s where the Result pattern comes in. It’s a functional approach that makes failure explicit, expressing directly in the method signature that something might go wrong. The trade-off? It’s up to the caller to inspect the result and act accordingly.

 

Here’s How to Avoid It

First we need to create a result class:

C#
				public class Result<T>
{
	private Result(bool isSuccess, T? value, string? error)
	{
		IsSuccess = isSuccess;
		Value = value;
		Error = error;
	}

	public bool IsSuccess { get; }
	public bool IsFailure => !IsSuccess;
	public T? Value { get; }
	public string? Error { get; }
	public bool HasError => !string.IsNullOrWhiteSpace(Error);

	public static Result<T> Success(T value) => new(true, value, null);

	public static Result<T> Failure(string error) => new(false, default, error);
}

			
  • IsSuccess and IsFailure give you a quick way to check the outcome.
  • Value holds the actual data if the operation succeeded.
  • Error contains a message if it failed.
  • The Sucess() and Failure() factory methods help you create results clearly.

This class is the foundation of the Result Pattern. But it can be improved in several ways, such as by introducing a custom error type to give you more control and structure.

C#
				public sealed record Error(string Code, string Description, ETypeError Type)
{
	public static readonly Error None = new(string.Empty, string.Empty, ETypeError.Failure);

	public static Error Failure(string code, string description) 
                        => new(code, description, ETypeError.Failure);

	public static Error Validation(string code, string description) 
                        => new(code, description, ETypeError.Validation);

	public static Error NotFound(string code, string description)
                        => new(code, description, ETypeError.NotFound);
}

			

Using a structured Error record helps you standardize error handling, include error codes for localization or client feedback, and keep your application consistent.

With the Result Pattern, you can make failure part of the method contract:

C#
				public sealed record Error(string Code, string Description, ETypeError Type)
{
	public static readonly Error None = new(string.Empty, string.Empty, ETypeError.Failure);

	public static Error Failure(string code, string description) 
                        => new(code, description, ETypeError.Failure);

	public static Error Validation(string code, string description) 
                        => new(code, description, ETypeError.Validation);

	public static Error NotFound(string code, string description)
                        => new(code, description, ETypeError.NotFound);
}

			

This makes your errors more expressive and allows better categorization of failure types such as Validation, Failure, or NotFound. You can then refactor your Result to return this Error type instead of just a string.

Here is the version of the method with result pattern:

C#
				public async Task<Result<NoticeResponse>> GetNoticeByIdAsync(int id)
{
	var notice = await noticeRepository.GetNoticeByIdAsync(id);

	if (notice is null)
	{
		return Result<NoticeResponse>
                    .Failure(Error.NotFound(CResponseCode.GenerateCode(nameof(NoticeService),
            								nameof(GetNoticeByIdAsync)),
            								CResponseMessage.NotFound));
	}

	var noticeResponse = notice.MapToResponse();

	return Result<NoticeResponse>.Success(noticeResponse);
}
			

Mapping Results to HTTP Responses

To integrate with APIs, you can map your Result to HTTP responses using an extension method:

C#
				public static class ResultExtensions
{
	public static IResult ToHttpResult<T>(this Result<T> result)
	{
		if (result.IsSuccess)
			return Results.Ok(result);

		return result.Error.Type switch
		{
			ETypeError.Validation => Results.BadRequest(result),
			ETypeError.NotFound => Results.NotFound(result),
			_ => Results.Problem(result.Error.Description)
		};
	}
}
			

Here’s how your controller (or endpoint) can use it cleanly:

C#
				app.MapGet("/notices/{id:int}", async (int id, INoticeService service) =>
{
	var result = await service.GetNoticeByIdAsync(id);
	return result.ToHttpResult();
});
			

This makes your endpoints more concise and consistent, while still handling errors appropriately.

Final Thoughts

The Result Pattern helps you write more reliable and intention-revealing code. By avoiding exceptions for expected failures, you make your system easier to reason about, maintain, and scale.

Whether you’re building APIs, services, or command handlers, embracing the Result Pattern can lead to cleaner and more robust solutions.

Have you used the Result Pattern in your projects?

Thank you for reading.

See you next time!

Share the Post:
plugins premium WordPress