# Ardalis.Result: replacing exception-driven service returns with a Result type

> Ardalis.Result is a small MIT-licensed C# library that gives .NET services a standard way to return success alongside several kinds of non-success outcomes, with companion packages for ASP.NET Core and FluentValidation.

**ardalis/Result** — A result abstraction that can be mapped to HTTP response codes if needed.

- Repository: https://github.com/ardalis/Result
- Stars: 1,044 · Forks: 129
- Language: C#
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/ardalis-result

## The problem with exceptions as a return channel

A service method that returns a customer has no natural way to say "not found" or "the last name is missing" or "this user may not create customers". The usual workaround is to throw a distinct exception type per failure mode, which forces every caller to write matching catch blocks. The README argues the result is unpleasant for the consumer and produces a lot of exceptions for situations that are not really exceptional, and it shows the shape of the code it wants to replace:

```csharp
[HttpGet]
public async Task<ActionResult<CustomerDTO>> GetCustomer(int customerId)
{
  try
  {
    var customer = _repository.GetById(customerId);
    
    var customerDTO = CustomerDTO.MapFrom(customer);
    
    return Ok(customerDTO);
  }
  catch (NullReferenceException ex)
  {
    return NotFound();
  }
  catch (Exception ex)
  {
    return new StatusCodeResult(StatusCodes.Status500InternalServerError);
  }
}
```

Note what that controller has to swallow: a `NullReferenceException` standing in for a not-found lookup, and a blanket `Exception` catch that turns everything else into a 500. The Result pattern gives those two cases a name each.

## Why tuples were not enough either

The README also considers the middle ground before settling on the Result pattern: returning a `Tuple` of the expected value alongside a status code and extra failure metadata. The argument against it is about reuse rather than about tuples being wrong. Tuples work for a single, flexible response, but they do not give you one standard shape to reuse across a service layer.

That is the whole promise, and it is worth being precise about it. `Result` gives a .NET service a single, reusable way to return both success and multiple kinds of non-success responses, and the type is arranged so those outcomes can be mapped to API response types later. The design keeps the domain service ignorant of the web layer, which matters for anything that has to work outside a controller.

The package structure follows the same separation. The core Ardalis.Result package has no dependencies on ASP.NET Core and can be used from any .NET Core application. The companion Ardalis.Result.AspNetCore package exists to enhance the pattern inside ASP.NET Core web API applications, and Ardalis.Result.FluentValidation is a third package for bridging validation results. A domain model project can reference the core package only, and the web project adds the rest.

## Building a Result for the common failure modes

The sample web project in the repository shows the pattern in use. Here is the shape from the README, a domain service that returns a forecast, with the not-found and invalid cases handled as values:

```csharp
public Result<IEnumerable<WeatherForecast>> GetForecast(ForecastRequestDto model)
{
    if (model.PostalCode == "NotFound") return Result<IEnumerable<WeatherForecast>>.NotFound();

    // validate model
    if (model.PostalCode.Length > 10)
    {
        return Result<IEnumerable<WeatherForecast>>.Invalid(new List<ValidationError> {
            new ValidationError
            {
                Identifier = nameof(model.PostalCode),
                ErrorMessage = "PostalCode cannot exceed 10 characters." 
            }
        });
    }

    var rng = new Random();
    return new Result<IEnumerable<WeatherForecast>>(Enumerable.Range(1, 5)
        .Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
    .ToArray());
}
```

The important detail is the generic parameter. `Result<IEnumerable<WeatherForecast>>` keeps the success payload typed, so a successful call still gives the caller a strongly typed collection, while the failure factories `NotFound()` and `Invalid(...)` return the same type and flow through the same return statement. The `ValidationError` carries an `Identifier` and an `ErrorMessage`, which is what a controller needs to turn a validation failure into a field-level response.

## Keeping the domain service free of ActionResult

The second half of the design is the mapping boundary. A domain service returns `Result<T>` and knows nothing about `ActionResult` or other MVC types. The controller is the only layer that knows what an HTTP response looks like, and the companion package supplies the translation between the two.

This is the practical reason to adopt the library rather than hand-roll a `Tuple` or a custom response class. The mapping lives in one place, and the shape of a failure is fixed across your API rather than decided separately in every controller. The README also documents a `Map` method for transforming results, with a dedicated write-up on transforming results with that method.

For teams using MediatR, the library docs cover combining Results with request handling, and the FluentValidation package covers the case where validation failures should flow through the same Result channel instead of being thrown as validation exceptions. A minimal API sample and a sample web API both exist under the `sample/` directory, along with unit test and functional test projects for each, so the intended usage is visible in code rather than only in prose.

## Getting the package and reading the docs

There is no build-from-source step in the README for this project, and that is correct, since the intended distribution is NuGet. The package identifiers are Ardalis.Result for the core library, Ardalis.Result.AspNetCore for the web API companion, and Ardalis.Result.FluentValidation for the validation integration. Each is published on nuget.org, and the README badges point at the specific package pages.

For a service library that must not reference the web stack, take the core package. For an ASP.NET Core API, add the AspNetCore companion. If you validate with FluentValidation, add the third. The repository itself is a .NET solution, `Ardalis.Result.sln`, with `src/` and `tests/` directories, central package versions in `Directory.Packages.props`, and a `sample/` tree that shows the intended project layout.

Documentation lives in the `/docs` folder of the repository and is published at result.ardalis.com. The README asks for issues when you want new docs and welcomes pull requests for documentation problems, and labels such a pull request with the documentation label. There is also a CONTRIBUTING.md and an AGENTS.md at the repository root if you plan to send a change.

## Version history, licence, and the cost of refactoring

The package is MIT licensed, which keeps the adoption conversation simple for commercial use, though the licence of your own code and any dependencies stays your own responsibility. The release tags are versioned as v9.1.0 in May 2024, v10.0.0 in September 2024, and v10.1.0 in October 2024.

The repository is not archived and the last push was on 2026-07-03, so work continues on the main branch even though the visible tags are older than that. That gap is worth checking before you pin a version: if you rely on a fix, confirm which tag contains it rather than assuming the newest tag is the current state of the branch.

The real cost of adopting this library is not the package, it is the refactor. Every service method that currently signals an expected failure by throwing has to change, and every caller has to be updated to inspect the result. That is a large, mostly mechanical diff across a service layer, and the payoff arrives only once controllers and callers have all been converted. The sample projects give you a small target to model the change on.

## Conclusion

Ardalis.Result fits teams building ASP.NET Core APIs that keep throwing exceptions for expected business outcomes such as a missing record or a validation failure, and who want those outcomes expressed as values the caller must handle. It does not fit a codebase that treats exceptions as the correct tool, or a project that needs richer failure payloads than a status, a message and a list of validation errors. Before adopting it, install Ardalis.Result.AspNetCore alongside the core package, read the docs in the /docs folder, and check the result.ardalis.com pages for the mapping behaviour your controllers depend on before you refactor a large service layer.

## FAQ

### What problem does Ardalis.Result solve in C#?

It gives .NET services a standard, reusable way to return success together with several kinds of non-success outcomes, so expected failures such as a missing record or a validation error do not have to be signalled with a distinct exception type per case.

### How do I install Ardalis.Result in an ASP.NET Core project?

The core Ardalis.Result package has no ASP.NET Core dependency and works in any .NET Core application, while the Ardalis.Result.AspNetCore companion package adds resources for mapping results inside web API applications. All three packages, including Ardalis.Result.FluentValidation, are published on nuget.org.

### How do I return a validation failure with Ardalis.Result?

Return the Invalid result with a list of ValidationError objects, each carrying an Identifier and an ErrorMessage. The sample service in the repository does this for a postal code longer than ten characters.

### Does Ardalis.Result depend on ASP.NET Core?

The core package does not. The Ardalis.Result.AspNetCore companion package exists specifically to enhance the pattern within ASP.NET Core web API applications, so a domain service project can depend on the core package alone.

### Is Ardalis.Result still maintained?

The repository is not archived and the last push was on 2026-07-03. The visible release tags are older, with v10.1.0 published on 2024-10-28, so check which tag contains a fix before pinning a version.

## Sources

- [ardalis/Result on GitHub](https://github.com/ardalis/Result)
- [Issues](https://github.com/ardalis/Result/issues)
- [License: MIT](https://github.com/ardalis/Result/blob/main/LICENSE)
- [README](https://github.com/ardalis/Result/blob/main/README.md)
- [Releases](https://github.com/ardalis/Result/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/ardalis-result
