A modern C# functional result type with structured errors, async workflows, LINQ support, deconstruction, and Source Link.
License
—
Deps
0
Install Size
—
Vulns
✓ 0
Published
Mar 4, 2026
$ dotnet add package BbQ.OutcomeA modern C# functional result type inspired by ErrorOr.
It builds on the excellent foundation of ErrorOr by adding async workflows, LINQ query syntax, deconstruction, Source Link, and multi-targeting — making error-aware programming feel like a first-class citizen in modern .NET.
ErrorOr pioneered the idea of replacing exceptions with a discriminated union of either a value or errors.
Outcome takes this idea further:
Error record with Code, Description, and Severity.BindAsync, MapAsync, CombineAsync for natural async pipelines.Select/SelectMany support for sync + async queries.(isSuccess, value, errors) for ergonomic handling.Success: 42 or Errors: [DIV_ZERO: Division by zero].netstandard2.0, net6.0, and net8.0.Error<T> helper properties from enums with the [QbqOutcome] attribute.var query =
from x in ParseAsync("10")
from y in DivideAsync(x, 2)
select y * 2;
var (ok, value, errors) = await query;
Console.WriteLine(ok
? $"Result: {value}"
: $"Errors: {string.Join("; ", errors.Select(e => e.Description))}");
Output:
Result: 10
dotnet add package BbQ.Outcome
The [QbqOutcome] attribute enables automatic generation of Error<TCode> helper properties for enums. This eliminates boilerplate and keeps error definitions DRY.
Mark your error enum with [QbqOutcome]:
[QbqOutcome]
public enum ApiErrorCode
{
/// <summary>
/// The requested resource was not found.
/// </summary>
NotFound,
/// <summary>
/// The user does not have permission to access this resource.
/// </summary>
Unauthorized,
/// <summary>
/// An internal server error occurred.
/// </summary>
InternalError
}The source generator automatically creates a static class ApiErrorCodeErrors with helper properties:
// Generated code (do not edit)
public static class ApiErrorCodeErrors
{
public static Error<ApiErrorCode> NotFoundError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.NotFound,
Description: "The requested resource was not found.",
Severity: ErrorSeverity.Error
);
public static Error<ApiErrorCode> UnauthorizedError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.Unauthorized,
Description: "The user does not have permission to access this resource.",
Severity: ErrorSeverity.Error
);
public static Error<ApiErrorCode> InternalErrorError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.InternalError,
Description: "An internal server error occurred.",
Severity: ErrorSeverity.Error
);
}You can specify error descriptions in two ways:
[QbqOutcome]
public enum ApiErrorCode
{
/// <summary>
/// The requested resource was not found.
/// </summary>
NotFound
}[QbqOutcome]
public enum ApiErrorCode
{
[System.ComponentModel.Description("Resource not found")]
NotFound
}The generator prioritizes [Description] attributes over XML comments. If neither is provided, it uses the enum member name as a fallback.
By default, all generated errors use ErrorSeverity.Error. You can customize the severity for individual enum members using the [ErrorSeverity(...)] attribute:
[QbqOutcome]
public enum ApiErrorCode
{
/// <summary>
/// Validation failed.
/// </summary>
[ErrorSeverity(ErrorSeverity.Validation)]
ValidationFailed,
/// <summary>
/// The requested resource was not found.
/// </summary>
NotFound, // Uses default ErrorSeverity.Error
/// <summary>
/// An internal server error occurred.
/// </summary>
[ErrorSeverity(ErrorSeverity.Critical)]
InternalError
}Generated code respects the specified severity levels:
// Generated code (do not edit)
public static class ApiErrorCodeErrors
{
public static Error<ApiErrorCode> ValidationFailedError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.ValidationFailed,
Description: "Validation failed.",
Severity: ErrorSeverity.Validation // Custom severity
);
public static Error<ApiErrorCode> NotFoundError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.NotFound,
Description: "The requested resource was not found.",
Severity: ErrorSeverity.Error // Default severity
);
public static Error<ApiErrorCode> InternalErrorError =>
new Error<ApiErrorCode>(
Code: ApiErrorCode.InternalError,
Description: "An internal server error occurred.",
Severity: ErrorSeverity.Critical // Custom severity
);
}The ErrorSeverity enum provides the following levels:
Info: Informational message; does not indicate a failure.Validation: Validation failure; the operation did not meet required conditions.Warning: Warning; the operation may have succeeded but with unexpected side effects.Error: Standard error; the operation failed and the error should be handled. (default)Critical: Critical error; the system may be in an inconsistent state.Error<T> construction.<summary> tags) or [Description] attributes.[Description] attributes or self-documenting XML comments.[ErrorSeverity(...)].{EnumMember}Error.Error<YourEnumType>.[QbqOutcome].[Description("...")] attribute if present<summary> from XML documentation comments[ErrorSeverity(...)] attribute (defaults to ErrorSeverity.Error)Error<T> properties using named parameters.public async Task<Outcome<User>> GetAndValidateUserAsync(Guid userId)
{
return await GetUserAsync(userId)
.BindAsync(user => ValidateUserAsync(user))
.BindAsync(user => EnrichUserAsync(user));
}var outcome = await CreateUserAsync(email, name);
string message = outcome.Match(
onSuccess: user => $"Created user {user.Name}",
onError: errors => $"Failed: {string.Join(", ", errors.Select(e => e.Description))}"
);var results = from user in GetUsersAsync()
from validated in ValidateAsync(user)
select validated;
var (ok, users, errors) = await results;var (success, value, errors) = outcome;
if (success)
{
Console.WriteLine($"Success: {value}");
}
else
{
foreach (var error in errors)
{
Console.WriteLine($"[{error.Severity}] {error.Code}: {error.Description}");
}
}When using BbQ.Cqrs, combine Outcome with commands and queries for comprehensive error handling:
// Error codes
[QbqOutcome]
public enum UserErrors
{
[Description("Email already in use")]
[ErrorSeverity(ErrorSeverity.Validation)]
EmailAlreadyExists
}
// Command returns Outcome<T>
public class CreateUserCommand : ICommand<Outcome<User>>
{
public string Email { get; set; }
}
// Handler uses source-generated errors
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, Outcome<User>>
{
public async Task<Outcome<User>> Handle(CreateUserCommand request, CancellationToken ct)
{
if (await UserExists(request.Email))
{
return UserErrorsErrors.EmailAlreadyExistsError.ToOutcome<User>();
}
return Outcome<User>.From(await CreateUser(request.Email));
}
}