ErrorApi.LanguageExt 1.0.1

dotnet add package ErrorApi.LanguageExt --version 1.0.1
                    
NuGet\Install-Package ErrorApi.LanguageExt -Version 1.0.1
                    
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="ErrorApi.LanguageExt" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ErrorApi.LanguageExt" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="ErrorApi.LanguageExt" />
                    
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 ErrorApi.LanguageExt --version 1.0.1
                    
#r "nuget: ErrorApi.LanguageExt, 1.0.1"
                    
#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 ErrorApi.LanguageExt@1.0.1
                    
#: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=ErrorApi.LanguageExt&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=ErrorApi.LanguageExt&version=1.0.1
                    
Install as a Cake Tool

ErrorApi

ErrorApi.LanguageExt

Keep returning Fin<T> — and get an OpenAPI document that finally says which errors each endpoint can return.

dotnet add package ErrorApi.LanguageExt

A language-ext Error carries a numeric code and a message. Neither says anything about HTTP, so the mapping ends up hand-written per endpoint — and the document still stops at 200 OK.

ErrorApi reads your error types at compile time, follows each endpoint handler through the call graph — including through IOrderService into its implementation — and writes the answer into OpenAPI.

This package is the language-ext half: it turns a Fin, an Either or a bare Error into a problem response that agrees with the document. It brings the generator with it; nothing else to install.

Before / after

Same feature, same Fin<T>, same services. Here is every line that changes.

1. The error types

Before:

using LanguageExt.Common;

public sealed record OrderNotFound(Guid Id) : Expected("Order not found", 404);
public sealed record OrderAlreadyPaid(Guid Id) : Expected("Order already paid", 409);

After — a bare attribute per type. Your Expected already carries the message and the status, so the generator reads them from the base constructor call; the wire code comes from the name under the catalog's prefix. Nothing is written twice:

using LanguageExt.Common;

[ErrorApi.ErrorCatalog("Orders")]
public static class OrderErrors
{
    [ErrorApi.Error]   // -> "Orders.NotFound", 404, "Order not found" — all read from the line below
    public sealed record NotFound(Guid Id) : Expected("Order not found", 404);

    [ErrorApi.Error]   // -> "Orders.AlreadyPaid", 409
    public sealed record AlreadyPaid(Guid Id) : Expected("Order already paid", 409);
}

Naming note. language-ext and ErrorApi both ship a type called Error, so spell the attribute out as [ErrorApi.Error] — or add using ErrorAttribute = ErrorApi.ErrorAttribute; to the file and keep writing [Error].

Everything stays overridable when you want it explicit: [ErrorApi.Error("Orders.NotFound", 404, Title = "…")] spells it all out, and [ErrorApi.ErrorDescription("…")] adds documentation prose to an otherwise bare entry.

2. The service

Not one line changes. This is the part people expect to have to rewrite, and do not:

public Fin<Order> GetById(Guid id) =>
    _orders.TryGetValue(id, out var order) ? order : new OrderErrors.NotFound(id);

3. The endpoint

Before — the status mapping is written by hand at every call site, and the .Produces calls are a second copy of the same knowledge that goes stale the first time the service gains a failure:

app.MapGet("/orders/{id:guid}", (Guid id, IOrderService s) =>
    s.GetById(id).Match<IResult>(
        order => TypedResults.Ok(order),
        error => error.Code switch
        {
            404 => TypedResults.Problem(statusCode: 404, title: "Order not found"),
            409 => TypedResults.Problem(statusCode: 409, title: "Order already paid"),
            _   => TypedResults.Problem(statusCode: 500),
        }))
    .Produces<ProblemDetails>(404)
    .Produces<ProblemDetails>(409);

After — the mapping comes from the catalog, and the documented responses are derived from the code:

app.MapGet("/orders/{id:guid}", (Guid id, IOrderService s) => s.GetById(id).ToHttpResult());

4. Start-up

Before:

builder.Services.AddOpenApi();

var app = builder.Build();
app.MapOpenApi();

After — one line, plus using ErrorApi.Interop; in the files that call ToHttpResult():

builder.Services.AddOpenApi();
builder.Services.AddErrorApi();   // generated overload — no reflection behind it

var app = builder.Build();
app.MapOpenApi();
app.MapErrorContract();           // optional: serves the TypeScript contract at /openapi/errors.ts

5. What the caller sees

Before, GET /orders/{id} promises only success in the document. The failures still happen; they are simply not written down anywhere a client can read:

"responses": {
  "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } } }
}

After:

"responses": {
  "200": { "description": "OK", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Order" } } } },
  "404": {
    "description": "Not Found — Orders.NotFound",
    "content": {
      "application/problem+json": {
        "schema": {
          "required": ["status", "code"],
          "properties": {
            "status": { "enum": [404], "type": "integer" },
            "code":   { "enum": ["Orders.NotFound"], "type": "string" }
          }
        },
        "examples": { "Orders.NotFound": { "value": { "status": 404, "code": "Orders.NotFound" } } }
      }
    }
  }
}

The response body is RFC 9457 with the machine-readable code alongside:

{ "title": "Order not found", "status": 404, "detail": "Order not found", "code": "Orders.NotFound" }

And the generated TypeScript turns a catch full of guesses into a union the compiler checks — add a failure server-side and the frontend build breaks instead of production:

// before: the shape is a guess, and the codes are folklore
catch (e) { if (e.status === 404) show("not found"); }
// after: generated from the same types the server compiled
export type GetOrdersByIdError = ApiProblem<"Orders.NotFound">;

How an error is resolved

Situation Result
The error's type is annotated The catalog's code, status and title. The error's Message becomes detail.
Not annotated, Code looks like a status That status is used, with Message as detail and the type name as the code.
Anything else 500, with Message as detail.

Resolution goes by type first, through a pattern switch the generator emits into your own assembly:

public ErrorDescriptor? FindErrorForInstance(object? instance) => instance switch
{
    global::Shop.OrderNotFound => _errors[1],
    _ => null,
};

That is the entire lookup — no reflection, nothing for the trimmer to keep alive. Falling back to the numeric code is a convenience for errors you did not write, not the intended path: an unannotated error gets a type name for a code, which is not a contract a client should depend on.

What you get

finResult.ToHttpResult();                          // Fin<T> and Task<Fin<T>>
finResult.ToHttpResult(order => Results.Ok(order));
finResult.ToNoContentResult();                     // 204
finResult.ToCreated(order => $"/orders/{order.Id}");   // 201, location built from the created value
finResult.ToCreatedAtRoute("GetOrder", order => new() { ["id"] = order.Id });
eitherResult.ToHttpResult();                       // Either<Error, T>
error.ToProblem();                                 // an Error on its own
error.ToErrorApiError();                           // resolve without producing a response

Every method that resolves an error takes an optional IErrorApiMetadata, so the behaviour is testable without standing up a host.

You also get, from the core package, a TypeScript contract with one union per endpoint:

export type GetOrdersByIdError = ApiProblem<"Orders.NotFound">;

A switch over problem.code stops compiling the moment the API gains a failure the client does not handle.

A runnable version of all this

dotnet run --project samples/Sample.LanguageExt.Api

The repository builds the same orders API four times — once on ErrorApi's own Result<T> and once per adapter — so you can diff the declaration styles against each other. They produce byte-identical contracts. Browse the document at http://localhost:5083/scalar, or read the generated model under obj/generated/ after a build.

Compatibility

Built against LanguageExt.Core 4.4.9, and verified in CI against 4.4.0 and 4.4.9. Targets net10.0 and is native-AOT clean.

On the v5 beta? ErrorApi.LanguageExt.V5 is the same surface compiled against the 5.x API, shipped as a prerelease that tracks the beta and goes stable the moment 5.0.0 does.

Full documentation

github.com/SideswipeN7/ErrorApi — how discovery works, the EAPI001EAPI013 diagnostics, the TypeScript contract, and the ErrorOr and OneOf adapters.

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.

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
1.0.1 98 8/31/2026
1.0.0 94 8/30/2026