RCi.ErrorAsValue 2.0.0

dotnet add package RCi.ErrorAsValue --version 2.0.0
                    
NuGet\Install-Package RCi.ErrorAsValue -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RCi.ErrorAsValue" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RCi.ErrorAsValue" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="RCi.ErrorAsValue" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RCi.ErrorAsValue --version 2.0.0
                    
#r "nuget: RCi.ErrorAsValue, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RCi.ErrorAsValue@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RCi.ErrorAsValue&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=RCi.ErrorAsValue&version=2.0.0
                    
Install as a Cake Tool

RCi.ErrorAsValue

CI NuGet License: MIT

A high-performance, zero-allocation Error-as-Value library for .NET.

Treat errors as explicit, structured values rather than throwing expensive exceptions. Combines the control flow of values with the diagnostic richness of exceptions: full stack traces by default, opt-out hot-path performance, self-healing boundaries, fluent arguments, and monadic composition.


Why Error-as-Value?

  • Zero-Allocation Happy Path: Ve<T> is a readonly record struct with no heap allocation on success.
  • Predictable Control Flow: Errors are explicit in method signatures - no hidden exceptions, no surprise crashes.
  • Safe & Diagnosable by Default: Full stack traces are captured automatically so production error logs are immediately actionable.
  • Opt-Out for Hot Loops: Tight loops (parsers, math algorithms) can pass captureStackTrace: false to run with zero allocations and pure ~5 ns speed.
  • Self-Healing Boundaries: When an un-traced error crosses into application logic and is wrapped via .Wrap(...), it automatically captures the call stack starting at that boundary.
  • Root Origin Preservation: err.Caller always tracks the exact member, file, and line where the error was first created.
  • Fluent Arguments with Expression Capture: .WithArg(userId) automatically captures the argument name "userId" via [CallerArgumentExpression].
  • Monadic & Functional Pipelines: Seamless composition via .Map(), .Bind(), and .Match().

Installation

dotnet add package RCi.ErrorAsValue

Supported frameworks: .NET 8.0, .NET 9.0, .NET 10.0.


Quick Start

using RCi.ErrorAsValue;

public sealed record User(int Id, string Name, string Email)
{
    public static readonly User Guest = new(0, "Guest", "guest@example.com");
}

public static Ve<User> FindUser(int id)
{
    if (id <= 0)
    {
        return Error.NewArgument("ID must be positive").WithArg(id);
    }

    if (id != 42)
    {
        return Error.NewNotFound("User does not exist").WithArg(id);
    }

    return new User(42, "Alice", "alice@example.com");
}

Handling Results

1. Go-Style Deconstruction
var (user, err) = FindUser(420);
if (err)
{
    Console.WriteLine($"Error: {err}"); // NotFound: User does not exist
    return;
}

Console.WriteLine($"Found: {user.Name}");
2. Pattern Matching via Ok / Failed
if (FindUser(420).Ok(out var user, out var err))
{
    Console.WriteLine($"User: {user.Name}");
}
else
{
    Console.WriteLine($"Failed: {err.Message}");
}
3. Functional / Monadic Pipelines
Ve<string> greeting = FindUser(42)
    .Bind(user => ValidateActive(user))
    .Map(user => $"Hello, {user.Name}!");

string message = greeting.Match(
    onOk: text => text,
    onError: err => $"Failed: {err.Message}"
);
4. Fallbacks and Unwrapping
var user1 = FindUser(420).UnwrapOr(User.Guest);
var user2 = FindUser(420).UnwrapOr(() => LoadGuestUser());
var user3 = FindUser(420).UnwrapOrDefault(); // null for reference types, default for value types

// Or throw an ErrorException if failure is truly exceptional:
var user4 = FindUser(42).UnwrapOrThrow();

Core Features

1. Hybrid Stack Trace Architecture

RCi.ErrorAsValue solves the tension between production diagnosability and hot-loop performance:

 [ Un-traced Inner Library ]      [ Application Boundary ]
        Variance                      StandardDeviation                  CalculateRisk
 (opts out for ~5 ns speed)         (bubbles up directly)             (wraps with context)
            │                                 │                                │
            ▼                                 ▼                                ▼
  Error.NewArgument(..., false) ──► if (err) return err; ──► err.Wrap("Risk failed")
                                                                       │
                                                       (Self-healing: captures boundary trace!)
Safe by Default

By default, all errors capture the full runtime call stack:

var err = Error.NewNotFound("Resource not found");
Console.WriteLine(err.HasStackTrace); // True
Hot-Path Opt-Out (~5 ns)

For algorithms, parsers, or Monte Carlo loops where errors are frequent and performance is critical:

return Error.NewArgument("Not enough items", captureStackTrace: false).WithArg(samples.Length);
  • err.HasStackTrace is false.
  • err.StackTrace falls back to the compile-time caller line (at Variance in MathLib.cs:line 6) with zero runtime overhead.
Self-Healing .Wrap(...)

When an un-traced error bubbles up to an application layer that wraps it:

return err.Wrap("Failed to calculate risk");
  • Wrap detects !err.HasStackTrace and automatically captures the stack trace starting at CalculateRisk!
  • If the error already had a stack trace, Wrap is a zero-overhead $O(1)$ operation that keeps the original root trace.
  • err.Caller continues to point to the exact root origin (Variance in MathLib.cs:line 6).
Ensuring a Stack Trace

If you receive an error without wrapping it and want to guarantee it has a stack trace before logging:

logger.LogError("{Error}", err.EnsureStackTrace());

2. Fluent Arguments with Expression Capture

Attach diagnostic data to errors without writing repetitive key strings:

int requestedId = 420;
string tenant = "acme";

// Parameter names "requestedId" and "tenant" are captured automatically!
var err = Error.NewNotFound("Entity not found")
    .WithArg(requestedId)
    .WithArg(tenant);

// Direct lookup:
int id = err.GetArg("requestedId"); // 420
if (err.TryGetArg<string>("tenant", out var tenantName))
{
    Console.WriteLine(tenantName); // acme
}

You can also pass explicit names or tuples:

err.WithArg(userId, "custom_name")
   .WithArgs(("retries", 3), ("timeoutMs", 5000));

3. Causal Chains & Kind Inspection (Wrap & Is)

Wrap lower-level errors with business context while preserving root causes:

public static Ve<Order> ProcessOrder(int userId)
{
    var (user, err) = FindUser(userId);
    if (err)
    {
        return err.Wrap("Failed to process order for customer");
    }

    return CreateOrder(user);
}
  • err.Kind / err.NodeKind: Effective resolved kind vs. this node's explicit override (null if none).
  • err.Message / err.NodeMessage: Full combined message chain vs. this node's explicit override.
  • err.Caller / err.NodeCaller: The origin site where the root error was created vs. the site where this node was wrapped.
  • err.Args / err.NodeArgs: All accumulated arguments across the chain vs. arguments attached to this node.
  • err.Chain: Enumerates all causal wrapper nodes from outermost to root.
  • err.Is(kind): Checks if any error in the causal chain matches a specific kind:
    if (err.Is(ErrorKind.NotFound))
    {
        // Handle 404 cleanly even if wrapped multiple times!
    }
    

4. Exception Interoperability

Seamlessly convert between exceptions and errors without losing type information:

// Convert Exception -> Error
try
{
    await httpClient.GetAsync("https://api.example.com");
}
catch (Exception ex)
{
    Error err = ex.ToError();
    
    // Inspect specific exception types in the causal chain:
    if (ExceptionError.TryGet<HttpRequestException>(err, out var httpEx))
    {
        Console.WriteLine(httpEx.StatusCode);
    }
}

// Convert Error -> Exception
throw err.ToException();

AggregateException with multiple inner exceptions are automatically flattened and chained as inner errors.


5. Structured Diagnostics (ToErrorDump)

Export clean DTOs ready for JSON logging (Serilog, Datadog, Seq) or HTTP Problem Details:

var (user, err) = FindUser(420);
if (err)
{
    ErrorDump dump = err.ToErrorDump();
    string json = JsonSerializer.Serialize(dump, new JsonSerializerOptions { WriteIndented = true });
    Console.WriteLine(json);
}

Output:

{
  "Kind": "NotFound",
  "Message": "User does not exist",
  "StackTrace": [
    "at FindUser in /repos/MyApp/UserService.cs:line 24",
    "at ProcessOrder in /repos/MyApp/OrderService.cs:line 50"
  ],
  "Args": [
    {
      "Name": "id",
      "Value": 420
    }
  ]
}

6. Centralized Telemetry (ErrorGlobalHook)

Monitor all created errors globally for logging, metrics, or alerting:

ErrorGlobalHook.OnError += (sender, err) =>
{
    Console.WriteLine($"[Telemetry] Error created: {err.Kind}: {err.Message}");
};

Subscriber exceptions are safely isolated and will never disrupt error creation.


Standard Error Kinds

RCi.ErrorAsValue provides convenient factory methods for common categories:

Factory Method Default Kind
Error.NewNotFound(...) ErrorKind.NotFound
Error.NewArgument(...) ErrorKind.Argument
Error.NewInternal(...) ErrorKind.Internal
Error.NewNotImplemented(...) ErrorKind.NotImplemented
Error.NewNotSupported(...) ErrorKind.NotSupported
Error.NewCancelled(...) ErrorKind.Cancelled
Error.NewUpstream(...) ErrorKind.Upstream
Error.NewException(...) ErrorKind.Exception
Error.New(kind, message) Custom Kind string

All factory methods accept bool captureStackTrace = true for hot-path opt-out.


License

This project is licensed under the MIT License.

Product 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 is compatible.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 93 9/14/2026
1.2.2 6,068 4/29/2025
1.2.1 2,305 11/16/2024
1.2.0 232 11/16/2024
1.1.0 357 11/4/2024
1.0.1 695 5/20/2024
1.0.0 307 5/8/2024