Skip to content

Reefact/first-class-errors

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

175 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

FirstClassErrors

🌍 Languages:
πŸ‡¬πŸ‡§ English (this file) | πŸ‡«πŸ‡· FranΓ§ais

.NET Standard 2.0 License: Apache 2.0


Turn your exceptions into structured, living knowledge about your system.

FirstClassErrors

FirstClassErrors is a .NET library that treats errors as first-class, documented, and diagnosable concepts β€” not just strings thrown at runtime.

It helps you:

  • express errors in a consistent and structured way
  • attach meaningful diagnostics to each error
  • keep error documentation close to the code
  • generate human-readable error documentation automatically

🚨 The problem

In most systems, errors are:

  • scattered across the codebase
  • described by ad-hoc messages
  • poorly documented
  • hard to troubleshoot
  • disconnected from support and operations

Over time, this leads to:

  • duplicated investigations
  • tribal knowledge
  • support teams guessing
  • developers reinventing error explanations

πŸ’‘ The idea

What if:

Every error in your system was explicitly described, structured, and documented β€” directly in code β€” and that documentation could be generated automatically?

FirstClassErrors introduces:

  • a rich error model
  • a structured diagnostic system
  • a DSL to document errors
  • a documentation extraction pipeline

Errors become:

not just failures, but documented knowledge units.

🧱 What this library provides

1️⃣ A richer error model

The Error carries:

  • a stable error code
  • a timestamp
  • three distinct messages β€” a mandatory public short summary, an optional public detail, and a mandatory internal diagnostic message
  • contextual data
  • structured diagnostics

The exception itself only exposes its .Error, and is designed to be:

  • logged consistently
  • understood by humans
  • used by tooling

Three messages, two audiences

An Error carries three messages but deliberately splits them across just two audiences β€” a public one (end users / API clients) and an internal one (logs, support, developers). The separation is enforced by construction, so what reaches a caller can never leak what is meant for developers and support:

Message Mandatory Audience Exposure
ShortMessage Yes End users / API clients A short public summary, safe to surface as-is (e.g. the title of an RFC 9457 problem detail).
DetailedMessage No End users / API clients A controlled public detail (e.g. the detail of an RFC 9457 problem detail), exposed only when the application explicitly chooses to. Must not carry sensitive or internal information.
DiagnosticMessage Yes Logs, support, developers The internal diagnostic message. May contain technical/operational detail (identifiers, offending values, internal state); it is never exposed to external clients by default.

The core model is HTTP-agnostic: DiagnosticMessage is never used as a default HTTP response body. When you turn an error into an exception with .ToException(), the resulting Exception.Message is the DiagnosticMessage β€” the developer- and log-facing text.

2️⃣ Structured diagnostics

Each error can declare possible causes and analysis leads:

  • What might have caused this error?
  • Is it likely input-related, system-related, or both?
  • Where should investigation start?

Diagnostics guide troubleshooting without hardcoding operational processes.

3️⃣ A DSL to describe errors

Errors are documented directly in code using a fluent API:

return DescribeError.WithTitle("Temperature below absolute zero")
                    .WithDescription("This error occurs when trying to instantiate a temperature with a value that is below absolute zero.")
                    .WithRule("Temperature cannot go below absolute zero because absolute zero is the point where particles have minimum possible energy.")
                    .WithDiagnostics(ValueObjectDiagnostic.Diagnostic)
                    .WithExamples(
                        () => BelowAbsoluteZero(-1, TemperatureUnit.Kelvin),
                        () => BelowAbsoluteZero(-280, TemperatureUnit.Celsius));

This is not just comments β€” it is structured, executable documentation.

4️⃣ Documentation extraction

The library includes a mechanism to scan assemblies and extract all declared error documentation:

  • linked to error factory classes
  • linked to factory methods
  • enriched with examples
  • ready to be rendered

This enables:

  • Markdown or JSON error catalogs (or any custom format via a renderer)
  • support-oriented documentation
  • living documentation generated from code
  • multi-language catalogs (opt-in) β€” see Internationalization

πŸ” Exception or not? You choose.

The library supports both:

  • throwing errors (traditional exception flow)
  • transporting errors without throwing via Outcome and Outcome<T>

This allows you to use exceptions as:

runtime signals or structured error data

depending on the context (domain logic, validation, pipelines, etc.).

🧩 Example

From the FirstClassErrors.Usage project:

[ProvidesErrorsFor(nameof(Temperature))]
public static class InvalidTemperatureError {

    [DocumentedBy(nameof(BelowAbsoluteZeroDocumentation))]
    internal static DomainError BelowAbsoluteZero(decimal invalidValue, TemperatureUnit invalidValueUnit) {
        return DomainError.Create(
                Code.TemperatureBelowAbsoluteZero,
                diagnosticMessage: $"Failed to instantiate temperature: the value {invalidValue} {invalidValueUnit} is below absolute zero.")
            .WithPublicMessage(
                shortMessage: "Temperature is invalid.",
                detailedMessage: $"The temperature {invalidValue} {invalidValueUnit} is below absolute zero.");
    }

    private static ErrorDocumentation BelowAbsoluteZeroDocumentation() {
        return DescribeError.WithTitle("Temperature below absolute zero")
                            .WithDescription("This error occurs when trying to instantiate a temperature with a value that is below absolute zero.")
                            .WithRule("Temperature cannot go below absolute zero because absolute zero is the point where particles have minimum possible energy.")
                            .WithDiagnostics(ValueObjectDiagnostic.Diagnostic)
                            .WithExamples(
                                () => BelowAbsoluteZero(-1, TemperatureUnit.Kelvin),
                                () => BelowAbsoluteZero(-280, TemperatureUnit.Celsius));
    }

    private static class Code {
        public static readonly ErrorCode TemperatureBelowAbsoluteZero = ErrorCode.Create("TEMPERATURE_BELOW_ABSOLUTE_ZERO");
    }
}

The factory returns a structured Error. When you need to throw it, you turn it into an exception with .ToException():

throw InvalidTemperatureError.BelowAbsoluteZero(-1, TemperatureUnit.Kelvin).ToException();

Here, the error, its meaning, its rule, its diagnostics, and example messages are all defined together β€” in code.

🎯 Who is this for?

FirstClassErrors is especially useful if:

  • you build complex business systems
  • you care about supportability
  • you want consistent error handling
  • you want documentation that doesn’t drift from code
  • you design with domain-driven thinking

πŸ” Analyzers

FirstClassErrors ships with a set of Roslyn analyzers (rule ids FCExxx) bundled in the NuGet package β€” reference the package and they run at build time, no extra install. They catch, before you run anything, the mistakes the runtime or the documentation pipeline would otherwise surface late or silently: duplicate error codes, [DocumentedBy] references that don't resolve, documented errors that never reach the catalog, and more.

See the analyzer rules reference.

πŸ“š Next steps

See the full documentation:

About

Tiny .NET library for structured, diagnosable exceptions.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors