RCi.ErrorAsValue
2.0.0
dotnet add package RCi.ErrorAsValue --version 2.0.0
NuGet\Install-Package RCi.ErrorAsValue -Version 2.0.0
<PackageReference Include="RCi.ErrorAsValue" Version="2.0.0" />
<PackageVersion Include="RCi.ErrorAsValue" Version="2.0.0" />
<PackageReference Include="RCi.ErrorAsValue" />
paket add RCi.ErrorAsValue --version 2.0.0
#r "nuget: RCi.ErrorAsValue, 2.0.0"
#:package RCi.ErrorAsValue@2.0.0
#addin nuget:?package=RCi.ErrorAsValue&version=2.0.0
#tool nuget:?package=RCi.ErrorAsValue&version=2.0.0
RCi.ErrorAsValue
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 areadonly record structwith 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: falseto 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.Calleralways 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.HasStackTraceisfalse.err.StackTracefalls 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");
Wrapdetects!err.HasStackTraceand automatically captures the stack trace starting atCalculateRisk!- If the error already had a stack trace,
Wrapis a zero-overhead $O(1)$ operation that keeps the original root trace. err.Callercontinues 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 (nullif 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 | 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 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. |
-
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.