Introduction to Result<T>
This article introduces the Result<T> pattern in C#, a way to explicitly represent the outcome of an operation that might succeed with a value or fail with an error. This pattern promotes clearer, more robust code compared to relying solely on exceptions for expected failure scenarios or returning nulls.
Defining the Result<T> Concept
Conceptually, Result<T> is a type that encapsulates two primary possibilities:
Success: The operation completed successfully and holds a value of type
T.Failure: The operation failed, and the
Result<T>holds structured error information, in this article using aProblemDetails.
We can model this using a base type and specific subtypes. Here's a minimal, usable implementation using ProblemDetails for errors.
The Failure State
The Failure<T> state indicates that the operation did not succeed. It carries a ProblemDetails object (from Microsoft.AspNetCore.Mvc). This object standardizes error reporting, allowing for an HTTP status code, a title, detailed messages, and even extension members to convey rich error information. This approach aligns well with building web APIs.
Note: This implementation is for educational purposes only. For production use, consider using established libraries instead of implementing your own solution. The ProblemDetails approach used here is just one implementation choice that may not be appropriate for all scenarios. This inheritance-based implementation will incur heap allocations for each Result<T> instance, which may impact performance in high-throughput scenarios. See: Memory Allocation and Management: Reference Types, Value Types, Arrays, and Linked Lists
Functional Pipelines vs. Traditional Error Handling
Traditionally, methods might signal failure by throwing exceptions or returning null. The Result<T> pattern encourages a more functional approach to using pipelines. See Using Exceptions for Control Flow
The Result<T> type itself primarily holds the state (Success or Failure). The behavior (chaining operations, transforming values, handling errors) is managed by extension methods like Map and FlatMap. This separation of data (the Result<T> state) and behavior (the pipeline methods) is a powerful aspect of this pattern.
Using Result<T> in Applications
This pattern is particularly useful when calling services, interacting with external systems, or performing any multistep process where any step might fail. Methods return Result<T> instead of T directly, making the possibility of failure explicit.
Example: Service Interface
A service method signature clearly indicates it returns a result:
Example: Using a Result<T> Pipeline
Here's a conceptual example showing how a pipeline might look when calling the service, using the extension methods defined later:
In this flow:
The process starts with an initial
Success<InputData>.FlatMapAsyncchains the service call. If_service.ProcessDataAsyncreturns aFailure, the pipeline short-circuits.MapAsynctransforms theProcessedDatainto aProcessedDataViewModelonly if the service call was successful.The final
Resultis handled:Successyields anOkObjectResult, andFailureyields anObjectResultcontaining theProblemDetails.
Core Pipeline Extension Methods: Map and FlatMap
Map and FlatMap (and their async counterparts) are fundamental to building Result<T> pipelines. Here are sample implementations:
Understanding Map
Map applies a transformation function (transformFunc) to the value inside a Success<T>, creating a Success<U>. If the input is Failure, Map bypasses the function and passes the Failure (with its ProblemDetails) through. It's for changing the type or value within a success state.
Understanding FlatMap
FlatMap (also commonly known as Bind in many functional programming libraries) chains operations where the next step (nextOperationFunc) also returns a Result. If the input is Success<T>, FlatMap executes the function and returns its result (Result<U>). Crucially, FlatMap prevents nesting like Result<Result<U>>, which would occur if Map were used with a function returning a Result. It "flattens" the output. If the input is Failure<T>, the function is skipped, and the Failure propagates.
Correlation with LINQ Operations
If you're familiar with LINQ, it may help to understand that:
Mapcorresponds to LINQ'sSelect: both transform a value inside a container (whether a collection or aResult). See: Three Common Functional Programming Operations on CollectionsFlatMapcorresponds to LINQ'sSelectMany: both handle nested containers and flatten the result. See: Using SelectMany()
The key difference is that while LINQ primarily deals with collections, Result<T> is a container that represents either success or failure. The operations conceptually behave in similar ways—transforming or chaining operations on values within a container - but Map and FlatMap include specific handling for the success/failure states.
Key Features of the Implementations
Error Handling : The
try-catchblocks inMapAsync,FlatMapAsync, and theSafeExecutehelpers ensure that exceptions are caught and converted intoFailureresults containingProblemDetails. Note that we're usingcatch (Exception ex)to catch all exceptions, which is often considered a bad practice in traditional exception handling. However, in this context, it's appropriate because the explicit goal of the Result pattern is to convert any unexpected failures into structuredFailureobjects rather than letting exceptions propagate. This ensures the pipeline remains robust and all errors are properly encapsulated.Short-Circuiting : The
switchexpressions naturally implement short-circuiting. If aFailureis encountered, the later transformation/operation function is not called, and theFailureis passed along the chain.
Summary
This article introduced the Result<T> pattern, using ProblemDetails for errors, as a robust way to handle operations with potential success or failure outcomes in C#. Key points include:
Representing outcomes explicitly using
Success<T>andFailure<T>(withProblemDetails).Separating the state (
Result<T>) from behavior (pipeline extension methods).Using functional pipeline methods like
Map(for transformation) andFlatMap(for chaining and avoiding nested results) to compose operations.Leveraging built-in error handling (exception-to-
Failureconversion) and short-circuiting within the pipeline methods.
For production systems, consider using established libraries such as OneOf, FluentResults, or LanguageExt that provide optimized, well-tested implementations of this pattern.