Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/amantinband/error-or
A simple, fluent discriminated union of an error or a result.
https://github.com/amantinband/error-or
Last synced: 7 days ago
JSON representation
A simple, fluent discriminated union of an error or a result.
- Host: GitHub
- URL: https://github.com/amantinband/error-or
- Owner: amantinband
- License: mit
- Created: 2022-05-31T11:15:21.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2024-08-21T01:57:25.000Z (4 months ago)
- Last Synced: 2024-11-28T14:05:03.402Z (14 days ago)
- Language: C#
- Size: 715 KB
- Stars: 1,626
- Watchers: 20
- Forks: 94
- Open Issues: 20
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE.md
Awesome Lists containing this project
- awesome-reference-tools - error-or
README
[![NuGet](https://img.shields.io/nuget/v/erroror.svg)](https://www.nuget.org/packages/erroror)
[![Build](https://github.com/amantinband/error-or/actions/workflows/build.yml/badge.svg)](https://github.com/amantinband/error-or/actions/workflows/build.yml) [![publish ErrorOr to nuget](https://github.com/amantinband/error-or/actions/workflows/publish.yml/badge.svg)](https://github.com/amantinband/error-or/actions/workflows/publish.yml)
[![GitHub contributors](https://img.shields.io/github/contributors/amantinband/error-or)](https://GitHub.com/amantinband/error-or/graphs/contributors/) [![GitHub Stars](https://img.shields.io/github/stars/amantinband/error-or.svg)](https://github.com/amantinband/error-or/stargazers) [![GitHub license](https://img.shields.io/github/license/amantinband/error-or)](https://github.com/amantinband/error-or/blob/main/LICENSE)
[![codecov](https://codecov.io/gh/amantinband/error-or/branch/main/graph/badge.svg?token=DR2EBIWK7B)](https://codecov.io/gh/amantinband/error-or)
---### A simple, fluent discriminated union of an error or a result.
`dotnet add package ErrorOr`
- [Give it a star ⭐!](#give-it-a-star-)
- [Getting Started 🏃](#getting-started-)
- [Replace throwing exceptions with `ErrorOr`](#replace-throwing-exceptions-with-errorort)
- [Support For Multiple Errors](#support-for-multiple-errors)
- [Various Functional Methods and Extension Methods](#various-functional-methods-and-extension-methods)
- [Real world example](#real-world-example)
- [Simple Example with intermediate steps](#simple-example-with-intermediate-steps)
- [No Failure](#no-failure)
- [Failure](#failure)
- [Creating an `ErrorOr` instance](#creating-an-erroror-instance)
- [Using implicit conversion](#using-implicit-conversion)
- [Using The `ErrorOrFactory`](#using-the-errororfactory)
- [Using The `ToErrorOr` Extension Method](#using-the-toerroror-extension-method)
- [Properties](#properties)
- [`IsError`](#iserror)
- [`Value`](#value)
- [`Errors`](#errors)
- [`FirstError`](#firsterror)
- [`ErrorsOrEmptyList`](#errorsoremptylist)
- [Methods](#methods)
- [`Match`](#match)
- [`Match`](#match-1)
- [`MatchAsync`](#matchasync)
- [`MatchFirst`](#matchfirst)
- [`MatchFirstAsync`](#matchfirstasync)
- [`Switch`](#switch)
- [`Switch`](#switch-1)
- [`SwitchAsync`](#switchasync)
- [`SwitchFirst`](#switchfirst)
- [`SwitchFirstAsync`](#switchfirstasync)
- [`Then`](#then)
- [`Then`](#then-1)
- [`ThenAsync`](#thenasync)
- [`ThenDo` and `ThenDoAsync`](#thendo-and-thendoasync)
- [Mixing `Then`, `ThenDo`, `ThenAsync`, `ThenDoAsync`](#mixing-then-thendo-thenasync-thendoasync)
- [`FailIf`](#failif)
- [`Else`](#else)
- [`Else`](#else-1)
- [`ElseAsync`](#elseasync)
- [Mixing Features (`Then`, `FailIf`, `Else`, `Switch`, `Match`)](#mixing-features-then-failif-else-switch-match)
- [Error Types](#error-types)
- [Built in error types](#built-in-error-types)
- [Custom error types](#custom-error-types)
- [Built in result types (`Result.Success`, ..)](#built-in-result-types-resultsuccess-)
- [Organizing Errors](#organizing-errors)
- [Mediator + FluentValidation + `ErrorOr` 🤝](#mediator--fluentvalidation--erroror-)
- [Contribution 🤲](#contribution-)
- [Credits 🙏](#credits-)
- [License 🪪](#license-)# Give it a star ⭐!
Loving it? Show your support by giving this project a star!
# Getting Started 🏃
## Replace throwing exceptions with `ErrorOr`
This 👇
```cs
public float Divide(int a, int b)
{
if (b == 0)
{
throw new Exception("Cannot divide by zero");
}return a / b;
}try
{
var result = Divide(4, 2);
Console.WriteLine(result * 2); // 4
}
catch (Exception e)
{
Console.WriteLine(e.Message);
return;
}
```Turns into this 👇
```cs
public ErrorOr Divide(int a, int b)
{
if (b == 0)
{
return Error.Unexpected(description: "Cannot divide by zero");
}return a / b;
}var result = Divide(4, 2);
if (result.IsError)
{
Console.WriteLine(result.FirstError.Description);
return;
}Console.WriteLine(result.Value * 2); // 4
```Or, using [Then](#then--thenasync)/[Else](#else--elseasync) and [Switch](#switch--switchasync)/[Match](#match--matchasync), you can do this 👇
```cs
Divide(4, 2)
.Then(val => val * 2)
.SwitchFirst(
onValue: Console.WriteLine, // 4
onFirstError: error => Console.WriteLine(error.Description));
```## Support For Multiple Errors
Internally, the `ErrorOr` object has a list of `Error`s, so if you have multiple errors, you don't need to compromise and have only the first one.
```cs
public class User(string _name)
{
public static ErrorOr Create(string name)
{
List errors = [];if (name.Length < 2)
{
errors.Add(Error.Validation(description: "Name is too short"));
}if (name.Length > 100)
{
errors.Add(Error.Validation(description: "Name is too long"));
}if (string.IsNullOrWhiteSpace(name))
{
errors.Add(Error.Validation(description: "Name cannot be empty or whitespace only"));
}if (errors.Count > 0)
{
return errors;
}return new User(name);
}
}
```## Various Functional Methods and Extension Methods
The `ErrorOr` object has a variety of methods that allow you to work with it in a functional way.
This allows you to chain methods together, and handle the result in a clean and concise way.
### Real world example
```cs
return await _userRepository.GetByIdAsync(id)
.Then(user => user.IncrementAge()
.Then(success => user)
.Else(errors => Error.Unexpected("Not expected to fail")))
.FailIf(user => !user.IsOverAge(18), UserErrors.UnderAge)
.ThenDo(user => _logger.LogInformation($"User {user.Id} incremented age to {user.Age}"))
.ThenAsync(user => _userRepository.UpdateAsync(user))
.Match(
_ => NoContent(),
errors => errors.ToActionResult());
```### Simple Example with intermediate steps
#### No Failure
```cs
ErrorOr foo = await "2".ToErrorOr()
.Then(int.Parse) // 2
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big") // 2
.ThenDoAsync(Task.Delay) // Sleep for 2 milliseconds
.ThenDo(val => Console.WriteLine($"Finished waiting {val} milliseconds.")) // Finished waiting 2 milliseconds.
.ThenAsync(val => Task.FromResult(val * 2)) // 4
.Then(val => $"The result is {val}") // "The result is 4"
.Else(errors => Error.Unexpected(description: "Yikes")) // "The result is 4"
.MatchFirst(
value => value, // "The result is 4"
firstError => $"An error occurred: {firstError.Description}");
```#### Failure
```cs
ErrorOr foo = await "5".ToErrorOr()
.Then(int.Parse) // 5
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big") // Error.Validation()
.ThenDoAsync(Task.Delay) // Error.Validation()
.ThenDo(val => Console.WriteLine($"Finished waiting {val} milliseconds.")) // Error.Validation()
.ThenAsync(val => Task.FromResult(val * 2)) // Error.Validation()
.Then(val => $"The result is {val}") // Error.Validation()
.Else(errors => Error.Unexpected(description: "Yikes")) // Error.Unexpected()
.MatchFirst(
value => value,
firstError => $"An error occurred: {firstError.Description}"); // An error occurred: Yikes
```# Creating an `ErrorOr` instance
## Using implicit conversion
There are implicit converters from `TResult`, `Error`, `List` to `ErrorOr`
```cs
ErrorOr result = 5;
ErrorOr result = Error.Unexpected();
ErrorOr result = [Error.Validation(), Error.Validation()];
``````cs
public ErrorOr IntToErrorOr()
{
return 5;
}
``````cs
public ErrorOr SingleErrorToErrorOr()
{
return Error.Unexpected();
}
``````cs
public ErrorOr MultipleErrorsToErrorOr()
{
return [
Error.Validation(description: "Invalid Name"),
Error.Validation(description: "Invalid Last Name")
];
}
```## Using The `ErrorOrFactory`
```cs
ErrorOr result = ErrorOrFactory.From(5);
ErrorOr result = ErrorOrFactory.From(Error.Unexpected());
ErrorOr result = ErrorOrFactory.From([Error.Validation(), Error.Validation()]);
``````cs
public ErrorOr GetValue()
{
return ErrorOrFactory.From(5);
}
``````cs
public ErrorOr SingleErrorToErrorOr()
{
return ErrorOrFactory.From(Error.Unexpected());
}
``````cs
public ErrorOr MultipleErrorsToErrorOr()
{
return ErrorOrFactory.From([
Error.Validation(description: "Invalid Name"),
Error.Validation(description: "Invalid Last Name")
]);
}
```## Using The `ToErrorOr` Extension Method
```cs
ErrorOr result = 5.ToErrorOr();
ErrorOr result = Error.Unexpected().ToErrorOr();
ErrorOr result = new[] { Error.Validation(), Error.Validation() }.ToErrorOr();
```# Properties
## `IsError`
```cs
ErrorOr result = User.Create();if (result.IsError)
{
// the result contains one or more errors
}
```## `Value`
```cs
ErrorOr result = User.Create();if (!result.IsError) // the result contains a value
{
Console.WriteLine(result.Value);
}
```## `Errors`
```cs
ErrorOr result = User.Create();if (result.IsError)
{
result.Errors // contains the list of errors that occurred
.ForEach(error => Console.WriteLine(error.Description));
}
```## `FirstError`
```cs
ErrorOr result = User.Create();if (result.IsError)
{
var firstError = result.FirstError; // only the first error that occurred
Console.WriteLine(firstError == result.Errors[0]); // true
}
```## `ErrorsOrEmptyList`
```cs
ErrorOr result = User.Create();if (result.IsError)
{
result.ErrorsOrEmptyList // List { /* one or more errors */ }
return;
}result.ErrorsOrEmptyList // List { }
```# Methods
## `Match`
The `Match` method receives two functions, `onValue` and `onError`, `onValue` will be invoked if the result is success, and `onError` is invoked if the result is an error.
### `Match`
```cs
string foo = result.Match(
value => value,
errors => $"{errors.Count} errors occurred.");
```### `MatchAsync`
```cs
string foo = await result.MatchAsync(
value => Task.FromResult(value),
errors => Task.FromResult($"{errors.Count} errors occurred."));
```### `MatchFirst`
The `MatchFirst` method receives two functions, `onValue` and `onError`, `onValue` will be invoked if the result is success, and `onError` is invoked if the result is an error.
Unlike `Match`, if the state is error, `MatchFirst`'s `onError` function receives only the first error that occurred, not the entire list of errors.
```cs
string foo = result.MatchFirst(
value => value,
firstError => firstError.Description);
```### `MatchFirstAsync`
```cs
string foo = await result.MatchFirstAsync(
value => Task.FromResult(value),
firstError => Task.FromResult(firstError.Description));
```## `Switch`
The `Switch` method receives two actions, `onValue` and `onError`, `onValue` will be invoked if the result is success, and `onError` is invoked if the result is an error.
### `Switch`
```cs
result.Switch(
value => Console.WriteLine(value),
errors => Console.WriteLine($"{errors.Count} errors occurred."));
```### `SwitchAsync`
```cs
await result.SwitchAsync(
value => { Console.WriteLine(value); return Task.CompletedTask; },
errors => { Console.WriteLine($"{errors.Count} errors occurred."); return Task.CompletedTask; });
```### `SwitchFirst`
The `SwitchFirst` method receives two actions, `onValue` and `onError`, `onValue` will be invoked if the result is success, and `onError` is invoked if the result is an error.
Unlike `Switch`, if the state is error, `SwitchFirst`'s `onError` function receives only the first error that occurred, not the entire list of errors.
```cs
result.SwitchFirst(
value => Console.WriteLine(value),
firstError => Console.WriteLine(firstError.Description));
```### `SwitchFirstAsync`
```cs
await result.SwitchFirstAsync(
value => { Console.WriteLine(value); return Task.CompletedTask; },
firstError => { Console.WriteLine(firstError.Description); return Task.CompletedTask; });
```## `Then`
### `Then`
`Then` receives a function, and invokes it only if the result is not an error.
```cs
ErrorOr foo = result
.Then(val => val * 2);
```Multiple `Then` methods can be chained together.
```cs
ErrorOr foo = result
.Then(val => val * 2)
.Then(val => $"The result is {val}");
```If any of the methods return an error, the chain will break and the errors will be returned.
```cs
ErrorOr Foo() => Error.Unexpected();ErrorOr foo = result
.Then(val => val * 2)
.Then(_ => GetAnError())
.Then(val => $"The result is {val}") // this function will not be invoked
.Then(val => $"The result is {val}"); // this function will not be invoked
```### `ThenAsync`
`ThenAsync` receives an asynchronous function, and invokes it only if the result is not an error.
```cs
ErrorOr foo = await result
.ThenAsync(val => DoSomethingAsync(val))
.ThenAsync(val => DoSomethingElseAsync($"The result is {val}"));
```### `ThenDo` and `ThenDoAsync`
`ThenDo` and `ThenDoAsync` are similar to `Then` and `ThenAsync`, but instead of invoking a function that returns a value, they invoke an action.
```cs
ErrorOr foo = result
.ThenDo(val => Console.WriteLine(val))
.ThenDo(val => Console.WriteLine($"The result is {val}"));
``````cs
ErrorOr foo = await result
.ThenDoAsync(val => Task.Delay(val))
.ThenDo(val => Console.WriteLine($"Finsihed waiting {val} seconds."))
.ThenDoAsync(val => Task.FromResult(val * 2))
.ThenDo(val => $"The result is {val}");
```### Mixing `Then`, `ThenDo`, `ThenAsync`, `ThenDoAsync`
You can mix and match `Then`, `ThenDo`, `ThenAsync`, `ThenDoAsync` methods.
```cs
ErrorOr foo = await result
.ThenDoAsync(val => Task.Delay(val))
.Then(val => val * 2)
.ThenAsync(val => DoSomethingAsync(val))
.ThenDo(val => Console.WriteLine($"Finsihed waiting {val} seconds."))
.ThenAsync(val => Task.FromResult(val * 2))
.Then(val => $"The result is {val}");
```## `FailIf`
`FailIf` receives a predicate and an error. If the predicate is true, `FailIf` will return the error. Otherwise, it will return the value of the result.
```cs
ErrorOr foo = result
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big"));
```Once an error is returned, the chain will break and the error will be returned.
```cs
var result = "2".ToErrorOr()
.Then(int.Parse) // 2
.FailIf(val => val > 1, Error.Validation(description: $"{val} is too big") // validation error
.Then(num => num * 2) // this function will not be invoked
.Then(num => num * 2) // this function will not be invoked
```## `Else`
`Else` receives a value or a function. If the result is an error, `Else` will return the value or invoke the function. Otherwise, it will return the value of the result.
### `Else`
```cs
ErrorOr foo = result
.Else("fallback value");
``````cs
ErrorOr foo = result
.Else(errors => $"{errors.Count} errors occurred.");
```### `ElseAsync`
```cs
ErrorOr foo = await result
.ElseAsync(Task.FromResult("fallback value"));
``````cs
ErrorOr foo = await result
.ElseAsync(errors => Task.FromResult($"{errors.Count} errors occurred."));
```# Mixing Features (`Then`, `FailIf`, `Else`, `Switch`, `Match`)
You can mix `Then`, `FailIf`, `Else`, `Switch` and `Match` methods together.
```cs
ErrorOr foo = await result
.ThenDoAsync(val => Task.Delay(val))
.FailIf(val => val > 2, Error.Validation(description: $"{val} is too big"))
.ThenDo(val => Console.WriteLine($"Finished waiting {val} seconds."))
.ThenAsync(val => Task.FromResult(val * 2))
.Then(val => $"The result is {val}")
.Else(errors => Error.Unexpected())
.MatchFirst(
value => value,
firstError => $"An error occurred: {firstError.Description}");
```# Error Types
Each `Error` instance has a `Type` property, which is an enum value that represents the type of the error.
## Built in error types
The following error types are built in:
```cs
public enum ErrorType
{
Failure,
Unexpected,
Validation,
Conflict,
NotFound,
Unauthorized,
Forbidden,
}
```Each error type has a static method that creates an error of that type. For example:
```cs
var error = Error.NotFound();
```optionally, you can pass a code, description and metadata to the error:
```cs
var error = Error.Unexpected(
code: "User.ShouldNeverHappen",
description: "A user error that should never happen",
metadata: new Dictionary
{
{ "user", user },
});
```
The `ErrorType` enum is a good way to categorize errors.## Custom error types
You can create your own error types if you would like to categorize your errors differently.
A custom error type can be created with the `Custom` static method
```cs
public static class MyErrorTypes
{
const int ShouldNeverHappen = 12;
}var error = Error.Custom(
type: MyErrorTypes.ShouldNeverHappen,
code: "User.ShouldNeverHappen",
description: "A user error that should never happen");
```You can use the `Error.NumericType` method to retrieve the numeric type of the error.
```cs
var errorMessage = Error.NumericType switch
{
MyErrorType.ShouldNeverHappen => "Consider replacing dev team",
_ => "An unknown error occurred.",
};
```# Built in result types (`Result.Success`, ..)
There are a few built in result types:
```cs
ErrorOr result = Result.Success;
ErrorOr result = Result.Created;
ErrorOr result = Result.Updated;
ErrorOr result = Result.Deleted;
```Which can be used as following
```cs
ErrorOr DeleteUser(Guid id)
{
var user = await _userRepository.GetByIdAsync(id);
if (user is null)
{
return Error.NotFound(description: "User not found.");
}await _userRepository.DeleteAsync(user);
return Result.Deleted;
}
```# Organizing Errors
A nice approach, is creating a static class with the expected errors. For example:
```cs
public static partial class DivisionErrors
{
public static Error CannotDivideByZero = Error.Unexpected(
code: "Division.CannotDivideByZero",
description: "Cannot divide by zero.");
}
```Which can later be used as following 👇
```cs
public ErrorOr Divide(int a, int b)
{
if (b == 0)
{
return DivisionErrors.CannotDivideByZero;
}return a / b;
}
```# [Mediator](https://github.com/jbogard/MediatR) + [FluentValidation](https://github.com/FluentValidation/FluentValidation) + `ErrorOr` 🤝
A common approach when using `MediatR` is to use `FluentValidation` to validate the request before it reaches the handler.
Usually, the validation is done using a `Behavior` that throws an exception if the request is invalid.
Using `ErrorOr`, we can create a `Behavior` that returns an error instead of throwing an exception.
This plays nicely when the project uses `ErrorOr`, as the layer invoking the `Mediator`, similar to other components in the project, simply receives an `ErrorOr` and can handle it accordingly.
Here is an example of a `Behavior` that validates the request and returns an error if it's invalid 👇
```cs
public class ValidationBehavior(IValidator? validator = null)
: IPipelineBehavior
where TRequest : IRequest
where TResponse : IErrorOr
{
private readonly IValidator? _validator = validator;public async Task Handle(
TRequest request,
RequestHandlerDelegate next,
CancellationToken cancellationToken)
{
if (_validator is null)
{
return await next();
}var validationResult = await _validator.ValidateAsync(request, cancellationToken);
if (validationResult.IsValid)
{
return await next();
}var errors = validationResult.Errors
.ConvertAll(error => Error.Validation(
code: error.PropertyName,
description: error.ErrorMessage));return (dynamic)errors;
}
}
```# Contribution 🤲
If you have any questions, comments, or suggestions, please open an issue or create a pull request 🙂
# Credits 🙏
- [OneOf](https://github.com/mcintyre321/OneOf/tree/master/OneOf) - An awesome library which provides F# style discriminated unions behavior for C#
# License 🪪
This project is licensed under the terms of the [MIT](https://github.com/mantinband/error-or/blob/main/LICENSE) license.