Result Pattern in C#

In this post, we're going to look at a somewhat functional way of programming, in which we combine functional programming with object-oriented programming. We owe this method, or pattern, to Scott Wlaschin, its original author—you can find more information on his website fsharpforfunandprofit.

 

The first thing we need to understand before starting is how our code works in a normal use case. For this example, we'll simulate the use case of creating a new user in the system.

 

And it's important to note: you do NOT need experience with functional programming to apply this pattern.

 

 

1 - Paths to follow

A - Happy path

As always, we start our design by thinking about the correct path to complete a task. In our case, it's pretty straightforward: we validate the user, save the user, and send a verification code to their email.

public string BasicAccountCreation(Account account){    ValidateNewAccount(account);    SaveUser(account);    SendCode(account);    return "Usuario anadido correctamente";}

happy path

 

B - Non-happy path

Of course, users don't always follow "the rules"—or maybe they're already registered. In these cases, we have to handle errors to avoid, for example, having duplicate users in our database.

 

non happy path

As we can see, we may encounter different errors at different points in our application. Here is a typical object-oriented implementation for error handling:

public string BasicAccountCreation(Account account){    string accountValidated = ValidateNewAccount(account);    if(!string.IsNullOrWhiteSpace(accountValidated))    {        return accountValidated;    }    bool isSaved = SaveUser(account);    if(!isSaved)    {        return "Error actualizando la base de datos";    }    bool isSent = SendCode(account);    if(!isSent)    {        return "Error Enviando el email";    }    return "Usuario anadido correctamente";}

As you can see, it's a lot of code—easy enough to follow here, but if we had many more validations, it could become quite complex.

 

If we analyze these flows in their respective steps, we see that the result is as follows: For the happy path, everything goes well, so we have a single entry and exit point.

 

Whereas for error analysis, we have several breaking points in our code.

flow completo

As mentioned at the beginning of the post, the main idea of this pattern is to blend the best of functional programming into our object-oriented applications.

 

To do this, we'll analyze the same process, but this time doing a functional flow analysis.

flow funcionalThis is the functional representation. Notice that the flow is similar but not the same, since in this scenario we have not just one response, but two at the end of our process.

 

 

2 - Functional design

To make our application return two results, what we need to do is have an object that holds both objects.

 

But, before we start coding like crazy, we have to understand the steps to follow, because there could be not just a single error, but several.

 

Based on our "non-happy path," we have the following path:

non happy path flowAs you can see, there are three types of errors: a validation error, a database error, and a mail server error.

 

So the resulting object would look something like the following, an object inside containing both "success" and the errors.

type result =     Success     || ErrorValidacion    || ErrorBaseDeDatos    || ErrorSMTPServer

Obviously, this code isn't final, since it's focused exclusively on this specific use case. We can compress this even more if we group all errors in a list or an error object that contains a list—our object would then only have two properties, independent of the use case.

type result =     Success    || Error

This solution is not 100% correct, though, as we return only a success indicator but none of the values, only the failures. If we want to return the element, we need to implement generics; this way, we can define Success<T>, where T is the type we want to return.

Type result<T> =    Success of T    || Error

 

3 - Pattern objective

Once we understand the pattern and its objective, we can define several goals:

  • Combine all errors in a single place.
  • Create smaller functions.
  • Display the data flow.
  • A single function per use case.

 

4 - How the Result Pattern (Railway Oriented Programming) works

It's also important to understand how it works. In step two, we saw the main objective is to create a wrapper for the type we want to return (Result<T>)—but why?

 

Here's where the functional programming part comes in. Basically, Result<T> will cause the type T to constantly change (or not, depending on the logic). To make this clearer, imagine this analogy: You have a car, it enters a function, and a bike comes out. Then that bike goes into another function, and a boat comes out, for example.

logica rop

It's simple: the input type of a function should be the output type of the previous function.

 

4.1 - Comparison

comparacion poo vs rop

Compared to a normal flow that checks for errors and returns when needed—although you don't always return, the program simply does not continue but jumps back—in our new pattern, it does "continue" (in quotes, because what it really does is skip all subsequent functions after a failure).

explicación rop

To see a bit more detail, in every method we receive both input parameters—the correct one and the failure. As long as everything is fine, our function keeps executing each method normally, but if an error is thrown at any moment, it switches to the error part.

rop funcional flow en aplicacion

We see we have "two" inputs and two possible outcomes. Each connection between two methods is made through a binding, which controls whether we're in the happy path or the failure path.

 

5 - How to apply ROP in C#

The pattern is mainly based on a single class we'll call Result<T>. So, the first thing we'll do is create a struct that holds our Result<T> and, of course, the errors.

 

Another thing to keep in mind is that, in our code, we may have methods that return void. These methods aren't compatible with the new pattern, because, as we've seen, every method must return a type. For this particular case, we have the Unit type, which does nothing and simulates void.

public sealed class Unit{    public static readonly Unit Value = new Unit();    private Unit() { }}public struct Result<T>{    public readonly T Value;    public static implicit operator Result<T>(T value) => new Result<T>(value);    public readonly ImmutableArray<string> Errors;    public bool Success => Errors.Length == 0;        public Result(T value)    {        Value = value;        Errors = ImmutableArray<string>.Empty;    }        public Result(ImmutableArray<string> errors)    {        if (errors.Length == 0)        {            throw new InvalidOperationException("debes indicar almenso un error");        }        Value = default(T);        Errors = errors;    }}

If we look closely, Result<T> contains the value of type T and the errors.

Note: the errors are just a simple string for this example, but we could have an Error type with codes, etc.

 

All the handling for this pattern is done using extension methods, allowing us to define when it's Success or Failure.

public static class Result{    public static readonly Unit Unit = Unit.Value;        public static Result<T> Success<T>(this T value) => new Result<T>(value);        public static Result<T> Failure<T>(ImmutableArray<string> errors) => new Result<T>(errors);    public static Result<T> Failure<T>(string error) => new Result<T>(ImmutableArray.Create(error));    public static Result<Unit> Success() => new Result<Unit>(Unit);    public static Result<Unit> Failure(ImmutableArray<string> errors) => new Result<Unit>(errors);    public static Result<Unit> Failure(IEnumerable<string> errors) => new Result<Unit>(ImmutableArray.Create(errors.ToArray()));    public static Result<Unit> Failure(string error) => new Result<Unit>(ImmutableArray.Create(error));}

There isn't much mystery to the code: just call Success or Failure as needed, and it creates the Result<T> type.

 

5.1 - Setting up the environment for ROP

Now let's see how to create the method that will decide whether program execution should go through the happy path or skip methods and go straight to the end.

 

In this post, we'll see the creation of the .Bind() method. For this, we'll use extension methods together with delegates, because the Bind method will receive a delegate (in other words, a method) as a parameter.

public static class Result_Bind{    public static Result<U> Bind<T, U>(this Result<T> r, Func<T, Result<U>> method)    {        try        {            return r.Success                ? method(r.Value)                : Result.Failure<U>(r.Errors);        }        catch (Exception e)        {            ExceptionDispatchInfo.Capture(e).Throw();            throw;        }    }}

Looking at the code, we see it's basically a simple if expressed as a ternary operator: if r (result) is Success (its error list is empty), run the method we pass as a parameter. Otherwise, it creates an object with the errors.

 

5.2 - Enhancing the environment for ROP

In this example, I haven't made the methods too complex—they currently work synchronously. If you want asynchronous support, you'd need to add Task<T> to both the method return value and input parameters and, of course, await the result.

Or, for example, if you wanted to create a .Then() method that ignores the delegate result (an Action delegate in this case) and returns the input value.

Or a .Map() where we map from one value to another.

public static async Task<Result<U>> Bind<T, U>(this Task<Result<T>> result, Func<T, Task<Result<U>>> method){    try    {        var r = await result;        return r.Success            ? await method(r.Value)            : Result.Failure<U>(r.Errors);    }    catch (Exception e)    {        ExceptionDispatchInfo.Capture(e).Throw();        throw;    }}public static Result<T> Then<T>(this Result<T> r, Action<T> action){    try    {        if (r.Success)        {            action(r.Value);        }        return r;    }    catch (Exception e)    {        ExceptionDispatchInfo.Capture(e).Throw();        throw;    }}public static Result<U> Map<T, U>(this Result<T> r, Func<T, U> mapper){    try    {        return r.Success            ? Result.Success(mapper(r.Value))            : Result.Failure<U>(r.Errors);    }    catch (Exception e)    {        ExceptionDispatchInfo.Capture(e).Throw();        throw;    }}

 

6 - Use case

The code is available on github.

Note: the code is synchronous for simplicity.

 

6.1 - Creating a user using OOP

For the example, I've created a use case or service, to which we'll simulate adding a user in our database. First, we'll implement the service using object-oriented programming:

public interface IAddUserPOOServiceDependencies{    bool AddUser(UserAccount userAccount);    bool EnviarCorreo(UserAccount userAccount);}/// <summary>/// Add the user using an object-oriented programming structure./// </summary>public class AddUserPOOService{    private readonly IAddUserPOOServiceDependencies _dependencies;        public AddUserPOOService(IAddUserPOOServiceDependencies dependencies)    {        _dependencies = dependencies;    }    public string AddUser(UserAccount userAccount)    {        var validacionUsuario = ValidateUser(userAccount);        if (!string.IsNullOrWhiteSpace(validacionUsuario))        {            return validacionUsuario;        }        var addUserDB = AddUserToDatabase(userAccount);        if (!string.IsNullOrWhiteSpace(addUserDB))        {            return addUserDB;        }        var sendEmail = SendEmail(userAccount);        if (!string.IsNullOrWhiteSpace(sendEmail))        {            return sendEmail;        }        return "Usuario añadido correctamente";    }    private string ValidateUser(UserAccount userAccount)    {        if (string.IsNullOrWhiteSpace(userAccount.FirstName))            return "El nombre propio no puede estar vacio";        if (string.IsNullOrWhiteSpace(userAccount.LastName))            return "El apellido propio no puede estar vacio";        if (string.IsNullOrWhiteSpace(userAccount.UserName))            return "El nombre de usuario no debe estar vacio";        return "";    }    private string AddUserToDatabase(UserAccount userAccount)    {        if (!_dependencies.AddUser(userAccount))        {            return "Error añadiendo el usuario en la base de datos";        }        return "";    }    private string SendEmail(UserAccount userAccount)    {        if (!_dependencies.EnviarCorreo(userAccount))        {            return "Error enviando el correo al email del usuario";        }        return "";    }}

As you can see, we have several problems. First, by returning a single type (a string, in this case), we limit the values we can return—we want to check several validations but can only return one at a time.

 

We could address this if, instead of returning a single string, we returned a List<string>; but in that case, if everything works fine, we're creating a list when we only need a single value.

 

Besides that, the code is somewhat hard to read. In this use case, we only have to check a couple of things, but the class is already quite long and harder to read. Still, the code works, albeit not beautifully—the main method AddUser isn't pretty but it does the job.

 

6.2 - Creating a user using ROP

Now let's do the same process, but using the "Railway Oriented Programming" pattern that we've covered throughout this post.

 

Before moving on, remember that this use case is intended to use our Result<T> type. I've also adjusted the dependency interface a bit to implement our new pattern.

 

And here is our implementation:

public interface IAdduserROPServiceDependencies{    Result<bool> AddUser(UserAccount userAccount);    Result<bool> EnviarCorreo(string email);}public class AdduserROPService{    private readonly IAdduserROPServiceDependencies _dependencies;    public AdduserROPService(IAdduserROPServiceDependencies dependencies)    {        _dependencies = dependencies;    }    public Result<UserAccount> AddUser(UserAccount userAccount)    {        return ValidateUser(userAccount)            .Bind(AddUserToDatabase)            .Bind(SendEmail)            .Map(_ => userAccount);    }    private Result<UserAccount> ValidateUser(UserAccount userAccount)    {        List<string> errores = new List<string>();        if (string.IsNullOrWhiteSpace(userAccount.FirstName))            errores.Add("El nombre propio no puede estar vacio");        if (string.IsNullOrWhiteSpace(userAccount.LastName))            errores.Add("El apellido propio no puede estar vacio");        if (string.IsNullOrWhiteSpace(userAccount.UserName))            errores.Add("El nombre de usuario no debe estar vacio");        return errores.Any()             ? Result.Failure<UserAccount>(errores.ToImmutableArray())             : userAccount;    }    private Result<string> AddUserToDatabase(UserAccount userAccount)    {        return _dependencies.AddUser(userAccount)            .Map(_ => userAccount.Email);    }    private Result<bool> SendEmail(string email)    {        return _dependencies.EnviarCorreo(email);    }}

Before explaining the code in more detail, notice how much the amount of code has been reduced. Less code usually means a cleaner and easier-to-understand codebase.

 

The main AddUser method is much smaller and easier to understand. Here, we're using our .Bind method to call each of the functions we want, and at the end of the sequence, we call .Map, which will run in the case everything went well, returning the user account.

 

The ValidateUser method underwent the biggest change; since it's the first to run, it's where we create our Result<T>—as Success if everything is fine, or as Failure if there are errors.

 

The AddUserToDatabase and SendEmail methods do the same as before, but now SendEmail only receives a string email; this is intentional, to show that the output of one method is the input to the next.

 

In these methods, we don't need to check for errors, because the dependency (interface) returns a Result<T>, so if there are errors, they're handled by .Map() or .Bind() in the main function.

 

 

Conclusion

In this post, we've seen how to blend functional and object-oriented programming in a way that greatly helps with code cleanliness, especially in error handling.

 

Personally, I recommend this pattern as it makes programming much simpler. In fact, this entire website is written with this pattern. Using it has allowed me to have much cleaner code, and when I have to change something, it barely takes me any time to read and fully understand what the code is doing.

 

The code for this example is on GitHub—I invite you to take it and try it in one of your projects. Over the next days/weeks I'll be expanding its functionality, for example, to make all methods asynchronous. If you have any improvement ideas, feel free to submit a Pull Request.

 

If you liked this content, please share (menu on the left).

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café