Maple.Result
1.0.80
See the version list below for details.
dotnet add package Maple.Result --version 1.0.80
NuGet\Install-Package Maple.Result -Version 1.0.80
<PackageReference Include="Maple.Result" Version="1.0.80" />
<PackageVersion Include="Maple.Result" Version="1.0.80" />
<PackageReference Include="Maple.Result" />
paket add Maple.Result --version 1.0.80
#r "nuget: Maple.Result, 1.0.80"
#:package Maple.Result@1.0.80
#addin nuget:?package=Maple.Result&version=1.0.80
#tool nuget:?package=Maple.Result&version=1.0.80
Result
An abstraction of the operation result which can be either successful (with or without a value) or failed (with an error).
It can be mapped to the HTTP response if needed.
Give it a star ⭐
Do you like it? Show your support by giving this project a star!
Why❓
Why the Result pattern?
This software design approach provides a structured and precise way to return the operation outcome—either successful or failed.
- No guessing what the
nullmeans! - Clear way of indicating whether the operation is successful!
- No need for misusing exceptions for not exceptional cases or controlling the flow!
Why this library?
Although, there are many existing implementation of the Result pattern in C#, I decided to create this implementation to address a few missing functionalities in other libraries, e.g.
- support for localization,
- support for consistent error categorization,
- support for precise error description,
- following standards (RFC 9457).
Quick Links
Quick Start
Adding the NuGet Package
Add the Maple.Result package to your project using the Package Manager in your IDE or the dotnet tool in the console:
dotnet add package Maple.Result
The Maple.Result package contains all the types and the core functionality of the Result pattern.
Using the Result
public async Task<Result> DeleteContactAsync(Guid id)
{
if (id == Guid.Empty)
{
return Error.Validation(
ErrorUri.Tag("tag:exampleapp.com,2026-01:errors:contact:delete:validation"),
"Cannot delete the contact.",
"Invalid ID of the contact to delete.",
detailTemplateId: "errors.contact.delete.id.validation");
}
var user = await _contactRepository.GetByIdAsync(id);
if (user is null)
{
return Error.NotFound(
ErrorUri.Tag("tag:exampleapp.com,2026-01:errors.contact.notFound"),
"Cannot delete the contact.",
"Contact with provided ID has not been found.",
detailTemplateId: "errors.contact.delete.id.notFound");
}
await _contactRepository.DeleteAsync(id);
return Result.Success();
}
Chaining operations
Available extension methods
IfSuccess()IfSuccessAsync()IfError()IfErrorAsync()Match()MatchAsync()
Example
var createUserResult = _userService.CreateUser(userData);
await createUserResult.MatchAsync(
user => _auditService.LogUserAddedAsync(user),
error => _auditService.LogErrorAsync(error));
var companyResult = createUserResult
.IfSuccess(() => _companyService.GetCompanyById(userData.CompanyId))
.Match(
company => _mapper.Map(company),
error => _mapper.Map(error));
The Result Pattern
Key Benefits
- Explicitness — clearly communicates (e.g., via the function signature) that the operation may fail; it forces developer to handle both scenarios: a success and a failure.
- Consistency — offers consistent type of error and consistent structure of responses.
- Composability — allows easier chaining of operations with extension methods, instead of multi-nested
ifblocks. - Predictable Error Handling — clearly defines categories of errors, making it easier to properly handle expected failures (e.g., validation errors, availability problems, a not-found resource).
- Support for localization — uses data structures that allow to localize messages by the client.
- Easier Testing — provides an easy way to verify in tests whether the operation was successful or what errors occurred.
- Improved Readability — limits the usage and scope of
try-catchblocks to places that actually can throw exceptions (e.g., calls to the database, cloud resources, disk operations, HTTP calls, etc.). - Improved Monitoring — allows to limit the number of thrown exceptions to truly exceptional cases making it easier to monitor them (if we don’t need to filter out exceptions used for the flow control).
- Improved Debugging — allows to enrich the error outcome with additional metadata.
- Follows Standards — follows RFC 9457: Problem Details for HTTP APIs and other industry standards.
Result vs. Exceptions
| Feature | Result | Exceptions |
|---|---|---|
| Use Cases | Expected, routine failures (e.g., validation, not found, missing data, authentication and authorization errors) | Rare, truly unexpected system or technical errors that usually indicate a bug or infrastructure problems (e.g., database connection loss, network issues) |
| Mechanism | Returns a structured record as the function outcome | Disrupts the normal program flow and bubbles up the call stack |
| Error Handling Style | Functional, return-value based | Imperative, runtime-based |
| Type Safety | Type-safe—the compiler knows a method can return an error | Not type-safe—errors are not visible in a method signature |
| Control flow | Clean, predictable flow maintained | Disrupted when an error occurs |
| Composition | Easy chaining of operations | Hard with multiple try-catch |
| Boilerplate | Can add some boilerplate code, which can be reduced with extension methods and other libraries | Centralizes error handling, keeping business logic clean of error checks |
| Performance | Better for frequent failures—avoids the overhead of exception handling | Slower—exceptions are expensive due to the cost of generating stack traces |
| Monitoring | Easy to distinguish expected errors from unexpected problems that needs potential attention | Requires filtering out expected errors from real issues. It can make it harder to investigate problems. |
From Stack Overflow discussion:
Have you ever tried to debug a program raising five exceptions per second in the normal course of operation ?
Exceptions are basically non-local
gotostatements with all the consequences of the latter. Using exceptions for flow control violates a principle of least astonishment, make programs hard to read
If you use exceptions for normal situations, how will you locate things that arereallyexception?
How To Use Result library
Result
Result vs Result<T>
Use:
Resultfor operations that does not return a value if successful (e.g.,DeleteBook(id)),Result<T>for operations that return a value if successful (e.g.,GetUser(id)).
Result
Use
return Result.Success();
to return successful Result without a value.
Result<T>
Use
public Result<User> GetUserById(int userId)
{
// code to retrieve the User object
return user;
}
to use the implicit operator to convert the value to Result<T>, or
return Result.FromValue(user);
to explicitly create a Result<T> with a value.
Example
public async Task<Result<User>> GetUserAsync(int userId)
{
try
{
var user = await _dbContext.Users.FirstOrDefaultAsync(x => x.id == userId);
if (user is null)
{
return Error.NotFound(
ErrorUri.Tag("tag:exampleapp.com,2026:errors:user:account:notfound"),
"Invalid user ID.",
"The user account with the provided ID has not been found.",
ErrorUri.Locator("https://api.exampleapp.com/errors/8783927589734857/details"),
"errors.user.account.notFound",
("accountId", "12345"), ("action", "account:update"));
});
}
return user;
}
catch (Exception ex)
{
_logger.LogError(ex, "An unexpected error occurred while retrieving user with ID {UserId}", userId);
return Error.InternalServerError(
ErrorUri.Tag("tag:exampleapp.com,2026:errors:user:account:geterror"),
"An unexpected error occurred.",
"An unexpected error occurred while getting the user account details.",
ErrorUri.Locator("https://api.exampleapp.com/errors/2393284590348/details"),
"errors.internal.dberror",
("accountId", userId.ToString()), ("action", "account:retrieve"), ("_exception", ex.Message));
}
}
Error
Use proper static methods to create an Error instance, e.g.
public Result UpdateUser(int userId, UpdateUserModel updateModel)
{
// code to retrieve the User object
if (user is null)
{
return Error.NotFound(
ErrorUri.Tag("tag:exampleapp.com,2026:errors:account:notfound"),
"Cannot update the account.");
}
// code to update the user
}
or
var error = Error.Validation(
ErrorUri.Tag("tag:exampleapp.com,2026:errors:signup:form"),
"Invalid data to create a user.",
"The user cannot be created because of missing or invalid data. Address the validation errors and try again.",
ErrorUri.Locator("http://exampleapp.com/errors/7894375839"),
"errors.signup.validation"
)
.AddDetail("#/email", "Email is required.", "errors.signup.validation.email.required")
.AddDetail("#/firstName", "At least 3 characters are required.", "errors.signup.validation.email.minLength", ("minLength", 3))
.AddDetail("#/mobile", "Phone number should be in format: (111) 111-1111.", "errors.signup.validation.mobile.format", ("format": "(111) 111-1111"));
Error Categories
The error categories are based on the HTTP status codes—that’s why those have been included in the table.
Although, mapping to HTTP responses is not part of this library and can be done differently.
| Category | Description | Solution | Example error | Corresponding HTTP status code |
|---|---|---|---|---|
| Conflict | The operation failed due to the data or state conflict (such as a unique constraint violation). Refresh data and try again. | Refresh the data, then try again if applicable. May require repeating the whole process. | Details have been modified. Please refresh the page. | 409 (Conflict) |
| Critical Error | Unexpected, critical error. | Contact customer support. | Payment failed. 3rd party API is not available. | 500 (Internal Server Error) |
| Failure | An expected error that is not critical to the operation. Might be related to e.g., a business logic rules, or the data that passes the validation, but still incorrect. | Even if the data is correct, the operation is not. | Invalid file format. Cannot delete the payment which processing has started. | 422 (Unprocessable Content) |
| Not Found | The resource was not found. | Invalid identifier. Try a different one. | Company with that name has not been found. | 404 (Not Found) |
| Not Implemented | The operation, feature or case is not implemented yet. | To be implemented in the future. | We don’t support debit cards yet. | 501 (Not Implemented) |
| Unauthorized | The authentication might be valid, but the identity does not have the required permissions. | Ask for permissions, or, a different user is required. | Action is not allowed for that user. | 403 (Forbidden) |
| Timeout | The operation timed out. | Heavy load. Try again later. | Server did not respond within 30 seconds. Try again later. | 408 (Request Timeout) |
| Unavailable | The resource is not available, but please try again later. | Some service is down. Please again later. | Report is not available—please try again later. | 503 (Service Unavailable) |
| Unauthenticated | The authentication is either missing or invalid. | Log in. | User is not logged in. | 401 (Unauthorized) |
| Validation | User input data is invalid. | Update the data and try again. | Email is required. | 400 (Bad Request) |
Error properties
| Property | Type | Description | Required | Example |
|---|---|---|---|---|
Category |
ErrorCategory |
A category of the error (one of predefined). | Yes | ErrorCategory.Validation |
TypeUri |
string |
A URI that identifies the type of the problem. When dereferenced, provides human-readable documentation about the problem. | Yes | https://app.example.com/errors/user/invalid-email |
Title |
string |
A short, human-readable summary of the problem type. It stays the same for the same type of error. | Yes | The provided value is not a valid email. |
Detail |
string |
The human-readable explanation specific to this occurrence of the problem. | No | Special character / is not allowed in the email address. |
DetailTemplated |
TemplatedMessage |
A structure to generate a localized detail message. | No | { "templateId": "errors.signup.email.specialcharacters", "params": { "invalidCharacter": "/" }} |
InstanceUri |
string |
A URI that identifies the specific occurrence of the problem. | No | https://api.exampleapp.com/errors/8783927589734857/details |
ErrorDetails |
IReadOnlyList<ErrorDetail> |
The collection of additional error details. | No | [{ "propertyPointer": "#/email", "detail": "Invalid format", "detailTemplated": { "templateId": "errors.signup.email.invalidFormat" }}] |
Type URI
It is a mandatory URI that identifies the type of the problem. It can be:
about:blank— the type is not specified,- URI Locator — dereferencing it SHOULD provide human-readable documentation for the problem type (use an absolute rather than a relative value) (e.g.,
https://exampleapp.com/docs/errors/signup/validation/email-required), - URI Tag — it is not dereferenceable and uniquely represents the type of the problem (e.g.,
tag:exampleapp.com,2026:errors.user.signup.validation.emailRequired).
Instance URI
It is an optional URI that the specific occurrence of the problem. It can be:
- URI Locator — dereferencing it SHOULD provide the problem details object or information about the problem occurrence in other formats (e.g.,
https://exampleapp/api/errors/982384293852/details), - URI Tag — a unique identifier for the problem occurrence which may be meaningful to the server but is opaque to the client (e.g.,
tag:exampleapp.com,2026:errors.func-sgnapi-p-01.logs.20260221.db382751-81b8-40da-8302-0a07dd16a5de).
Support for multiple error details / validation errors
if (string.IsNullOrWhiteSpace(userInput.email))
{
error.AddDetail(
"#/email",
"The email is required.",
"errors.user.input.email.required");
}
if (userInput.email.Length < 8)
{
error.AddDetail(
"#/email",
"The email must be at least 8 characters long.",
"errors.user.input.email.minLength",
("minLength", 8));
}
where the parameters are:
| Parameter | Type | Description | Required | Example |
|---|---|---|---|---|
propertyPointer |
string |
JSON Pointer to the input data. | no | #/firstName |
detail |
string |
A human-readable explanation of this error detail. | yes | Mininum 5 characters are required. |
messageId |
string |
A message template ID. | no | errors.signup.validation.firstName.minLength |
namedValues |
(string, object)[] |
The collection of named values for the message template. | no | ("minLength": 5) |
Support for localization
The localization can be achieved by generating messages based on the template.
Example
var error = Error.Unauthorized(
"tag:exampleapp.com,2026:errors:contact:delete:authorization:missing",
"User is not authorized to delete a contact.",
"The ‘contact_delete’ permission is required to delete a contact.",
detailTemplateId: "errors.contact.delete.unauthorized",
detailNamedValues: ("requiredPermission", "contact_delete"));
It uses:
- a template with ID:
errors.contact.delete.unauthorized - and named values:
requiredPermission:contact_delete
| en_ca | fr_ca | es | zh_cn | |
|---|---|---|---|---|
| Template | The ‘{requiredPermission}’ permission is required to delete a contact. |
L'autorisation «{requiredPermission}» est requise pour supprimer un contact. |
Se requiere el permiso «{requiredPermission}» para eliminar un contacto. |
删除联系人需要“{requiredPermission}”权限。 |
| Localized message | The ‘contact_delete’ permission is required to delete a contact. |
L'autorisation «contact_delete» est requise pour supprimer un contact. |
Se requiere el permiso «contact_delete» para eliminar un contacto. |
删除联系人需要“contact_delete”权限。 |
Controlling the execution flow
You can use:
IsSuccess()method of theResult,- extension methods:
IsSuccess(),IsSuccessAsync(),IsError(),IsErrorAsync(),Match(),MatchAsync().
var userResult = await _userService.GetUserAsync(userData);
var userAddedResult = await userResult.IfSuccessAsync(async (user) =>
{
return await _companyService.AddUserAsync(user);
});
var userTokenResult = await userAddedResult.MatchAsync(
async (user) => await _loginService.GetUserTokenAsync(user),
async (error) => await _auditService.LogErrorAsync(error));
Mapping to HTTP responses
Mapping to HTTP responses is not part of this library.
Mapping of the Result and Error to HTTP responses will be implemented as a separate library.
FAQ
When to use a Result and when an Exception?
It is up to you where to draw the line between expected failure and unexpected exception.
In some cases, a missing file is just a resource Not Found error (e.g., users tries to open a non-existing document); in others, it might be an unexpected exception (e.g., a DLL file or expected configuration is missing).
My suggestion is:
- use exceptions for cases that require someone’s attention (e.g., database unavailable, connection string to a crucial resource is
null, unexpected case that indicates a bug, expired API keys that require manual update), - use Result for cases that you won’t to be bothered with (e.g., user input validation, user authentication/authorization errors, handling transient errors that will be recovered by retry, etc.).
Why Unauthenticated corresponds to the 401 (Unauthorized) HTTP status code?
By the HTTP reference from Mozilla:
The HTTP
401 Unauthorized** **client error response status code indicates that a request was not successful because it lacks valid authentication credentials for the requested resource.
A 401 Unauthorized is similar to the 403 Forbidden response, except that a 403 is returned when a request contains valid credentials, but the client does not have permissions to perform a certain action.
What is the difference between a Critical Error and an Exception?
It is to a developer to draw a line between the errors that are critical and are handled in one way or another, and the ones that are unhandled and disrupt the normal program flow and bubbles up the call stack.
Why do methods like Bind(), Else(), Map(), Switch, Then() and others are not available?
This library minimizes the amount of extension methods:
Bind()can be replaced with e.g.,IfSuccess<T, TNext>(Result<T>, Func<T, TNext>),Else()can be replaced with e.g.,IfError<T>(Result, Func<Error, T>),Map()can be replaced with e.g.,IfSuccess<T, TNext>(Result<T>, Func<T, TNext>),Switch()can be replaced with e.g.,Match<T>(Result<T>, Action<T>, Action<Errror>).
Learn More
Documentation
- Maple.Result —
Result,Error, and other types - Maple.Result.Extensions — extension methods
See also
- Problem Details for HTTP APIs - RFC 7807 is dead, long live RFC 9457
- tag URI scheme
- RFC 1738: Uniform Resource Locators (URL)
- RFC 3986: Uniform Resource Identifier (URI): Generic Syntax
- RFC 4151: The 'tag' URI Scheme
- RFC 6901: JavaScript Object Notation (JSON) Pointer
- RFC 9457: Problem Details for HTTP APIs
Articles
- Exception Vs Result Pattern. The Exception vs Result Pattern… | by Shreyans Padmani | Medium
- The Result Pattern: Simplifying Error Handling in Your Code | by Adam Hancock | Medium
- Exceptions for flow control in C# · Enterprise Craftsmanship
- language agnostic - Why not use exceptions as regular flow of control? - Stack Overflow
Contribution
Please contact author: engineer(at sign)blumail(dot)me
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Maple.Result:
| Package | Downloads |
|---|---|
|
Maple.Result.Extensions.HttpClient
A collection of extension methods to map HttpClient’s HttpResponseMessage to Maple.Result. |
|
|
Maple.Result.Extensions.AspNetCore
Provides mapping of the Maple.Result to the ASP.NET Core ActionResult. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 1.39.1 | 193 | 3/10/2026 | |
| 1.38.16 | 251 | 3/6/2026 | |
| 1.38.14 | 92 | 3/5/2026 | |
| 1.38.9 | 118 | 3/2/2026 | |
| 1.38.7 | 118 | 2/28/2026 | |
| 1.38.0 | 98 | 2/27/2026 | |
| 1.36.0 | 89 | 2/27/2026 | |
| 1.35.0 | 230 | 2/19/2026 | |
| 1.32.0 | 90 | 2/19/2026 | |
| 1.28.0 | 103 | 2/12/2026 | |
| 1.24.9 | 100 | 1/30/2026 | |
| 1.0.80 | 98 | 1/30/2026 | |
| 1.0.75 | 97 | 1/29/2026 | |
| 1.0.72 | 97 | 1/29/2026 | |
| 1.0.42 | 106 | 1/21/2026 | |
| 1.0.40 | 95 | 1/21/2026 | |
| 1.0.38 | 90 | 1/21/2026 | |
| 1.0.36 | 109 | 1/21/2026 | |
| 1.0.33 | 118 | 1/16/2026 | |
| 1.0.28 | 146 | 1/16/2026 |