Introduction
This is a short write up of what I consider good practices when using FluentResult with ASP.NET Core and the result pattern.
If you haven't heard of it before, FluentResults is a result-based error handling library for .NET that replaces exceptions-as-control-flow with explicit success/failure objects, implementing the result pattern.
Use strongly-typed erros
Avoid passing simple strings to Result.Fail(). Instead, create classes for domain-specific failure scenarios. This allows you to check for specific error types later in the UI or API layer.
public class UserUnderageError(int age) : Error($"User is {age}, but must be 18.")
{
public int Age { get; set; } = age;
}
// Usage
if (age < 18) return Result.Fail(new UserUnderageError(age));Distinguish between Error and ExceptionalError
FluentResult has a build-in ExceptionalError. Use it when a result fails because of an actual Exception you caught. This preserves the stack trace.
try {
// ...
} catch (Exception ex) {
return Result.Fail(new ExceptionalError(ex));
}
Alternatively you can use CausedBy, like so
try {
// ...
} catch (Exception ex) {
return Result.Fail(new Error(ex.Message).CausedBy(ex);
}The structure of the Errors property will look slightly different

Enrich errors by wrapping as a single chain using CausedBy
Instead of adding new top-level errors and keeping the previous one like so...
var result = SomeOp();
return Result.Fail(new Error("Some low level operation failed"))
.WithErrors(result.Errors):... prefer wrapping as a single chain using CausedBy which keeps exactly one top-level error and the previosu errors are attached as the causes.
This preserves nesting and keeps the error list clean. It aligns well with the Clean Architecture boundaries and you can render a clean "stack" or failures later.
private static Result Wrap(Result inner, string message, object? meta = null)
{
var outer = new Error(message);
if (meta is not null)
outer.WithMetadata("Context", meta);
// keep the original errors as causes (nested), not siblings
foreach (var e in inner.Errors)
outer.CausedBy(e);
return Result.Fail(outer);
}Then use it like
public Result SomeBusinessOperation()
{
var result = Level2();
return result.IsFailed ? Wrap(result, "Some business operation failed") : result;
}
private Result Level2()
{
var result = Level1();
return result.IsFailed ? Wrap(result, "Level 2 operation failed") : result;
}
... 
Use Bind to avoid the pyramid of doom
Instead of nasting multiple if (result.IsFailed) statements, use Bind to chain operations. This keeps the happy path flat and readable.
// Nah...
var userResult = _service.GetUser(id);
if (userResult.IsSuccess)
{
var validation = _validator.Validate(userResult.Value);
if (validation.IsSuccess)
{
// ...
}
}
// Better...
return _service.GetUser(id)
.Bind(user => _validator.Validate(user))
.Bind(user => _repository.Update(user));Never return null inside a Result
Things like this can drive you crazy, please just don't do it. A Result.Ok<T>(null) is an anti-pattern that leads to NullReferenceException even when IsSuccess is true. If a value might not exist (like in a Find operation), consider return a specific NotFoundError instead of success with a null value.
// Better than returning Ok(null)
public Result<Person> GetPerson(int id)
{
var person = _db.Find(id);
return person ?? Result.Fail(new PersonNotFoundError(id));
}Avoid overusing the result pattern for private methods
You don't have to use Result<T> for each and every private helper method. If a method is a simple utility that cannot fail in a domain sense, just return the raw type. Prefer to use Result for business logic.
Further reading

