[{"content":"Introduction In an earlier post, I created Result\u0026lt;TValue, TError\u0026gt;, a generic, allocation-free struct. On its own, it supports only procedural programming. Call something, check Success or Failure, branch and repeat. This is the same boilerplate as Go’s if err != nil.\nIn functional languages, a result is a value like any other, so operations can be composed on it directly. F# includes a Result module for this. This post discusses extension methods that reimplement map, mapError, bind, iter, iterError, defaultValue and defaultWith for Result\u0026lt;TValue, TError\u0026gt;. They let you chain steps without checking Success or Failure after each call.\nFunctional extensions The extensions are straightforward. The hard part is ensuring the overloads cover enough use cases so that callers rarely need anything the library does not provide. To address this, I identified up to four aspects in which the overloads of each method differ:\nThe type being extended can be Result\u0026lt;TValue, TError\u0026gt;, Task\u0026lt;Result\u0026lt;TValue, TError\u0026gt;\u0026gt; or ValueTask\u0026lt;Result\u0026lt;TValue, TError\u0026gt;\u0026gt; (three variants). The kind of delegate can be synchronous, return a Task or return a ValueTask (three variants). The number of extra arguments supplied to the delegate ranges from 0 to 3 (four variants). Whether the delegate takes what the result holds (two variants). Not every aspect applies to every extension. Knowing which aspects apply gives the number of overloads to expect, so a missing or an extra one is easier to spot.\nMap and MapError Map converts the success value into a new one and on failure returns the error unchanged. MapError is the opposite: it turns the error into a new one and on success returns the value unchanged:\npublic static Result\u0026lt;TNewValue, TError\u0026gt; Map\u0026lt;TValue, TError, TNewValue\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, Func\u0026lt;TValue, TNewValue\u0026gt; func) where TValue : notnull where TError : notnull where TNewValue : notnull { return result.Success ? func(result.Value) : result.Error; } public static Result\u0026lt;TValue, TNewError\u0026gt; MapError\u0026lt;TValue, TError, TNewError\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, Func\u0026lt;TError, TNewError\u0026gt; func) where TValue : notnull where TError : notnull where TNewError : notnull { return result.Success ? result.Value : func(result.Error); } Both vary in:\nThe type being extended. The kind of delegate. The number of extra arguments. Whether the delegate takes what the result holds. That results in 3 × 3 × 4 × 2 = 72 overloads each.\nBind On success, Bind returns the new Result from its delegate; otherwise, the error:\npublic static Result\u0026lt;TNewValue, TError\u0026gt; Bind\u0026lt;TValue, TError, TNewValue\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, Func\u0026lt;TValue, Result\u0026lt;TNewValue, TError\u0026gt;\u0026gt; func) where TValue : notnull where TError : notnull where TNewValue : notnull { return result.Success ? func(result.Value) : result.Error; } Bind varies in:\nThe type being extended. The kind of delegate. The number of extra arguments. Whether the delegate takes what the result holds. That results in 3 × 3 × 4 × 2 = 72 overloads.\nIter and IterError Iter performs a side effect by calling a delegate with the success value. IterError does the same with the error. Both return the result they were given:\npublic static Result\u0026lt;TValue, TError\u0026gt; Iter\u0026lt;TValue, TError\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, Action\u0026lt;TValue\u0026gt; action) where TValue : notnull where TError : notnull { if (result.Success) { action(result.Value); } return result; } public static Result\u0026lt;TValue, TError\u0026gt; IterError\u0026lt;TValue, TError\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, Action\u0026lt;TError\u0026gt; action) where TValue : notnull where TError : notnull { if (result.Failure) { action(result.Error); } return result; } Both vary in:\nThe type being extended. The kind of delegate. The number of extra arguments. Whether the delegate takes what the result holds. That results in 3 × 3 × 4 × 2 = 72 overloads each.\nDefaultValue and DefaultWith DefaultValue returns the success value or a fallback value when the result is a failure. DefaultWith calculates that fallback from the error with a delegate:\npublic static TValue DefaultValue\u0026lt;TValue, TError\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, TValue defaultValue) where TValue : notnull where TError : notnull { return result.Success ? result.Value : defaultValue; } public static TValue DefaultWith\u0026lt;TValue, TError\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, TValue defaultValue) where TValue : notnull where TError : notnull { return result.Success ? result.Value : defaultValue; } DefaultValue has one overload for each type being extended, three in all.\nDefaultWith varies in:\nThe type being extended. The kind of delegate. The number of extra arguments. Whether the delegate takes what the result holds. That results in 3 × 3 × 4 × 2 = 72 overloads.\nMatch To reduce a result to a single value, I added Match, which handles both cases:\npublic static TOutput Match\u0026lt;TValue, TError, TOutput\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, Func\u0026lt;TValue, TOutput\u0026gt; onSuccess, Func\u0026lt;TError, TOutput\u0026gt; onFailure) where TValue : notnull where TError : notnull { return result.Success ? onSuccess(result.Value) : onFailure(result.Error); } Match varies in:\nThe type being extended. The kind of delegate, which applies to both handlers. The number of extra arguments. That results in 3 × 3 × 4 = 36 overloads.\nAsync lambda overload ambiguity With this many overloads, some become ambiguous, specifically those for Task and ValueTask delegates. An async lambda’s return type depends on the delegate it is passed to, so the same lambda can return Task or ValueTask. When overloads for both return types exist, the compiler cannot choose between them and reports the call as ambiguous:\npublic static async Task\u0026lt;Result\u0026lt;TNewValue, TError\u0026gt;\u0026gt; Map\u0026lt;TValue, TError, TNewValue\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, Func\u0026lt;TValue, Task\u0026lt;TNewValue\u0026gt;\u0026gt; func) where TValue : notnull where TError : notnull where TNewValue : notnull public static async ValueTask\u0026lt;Result\u0026lt;TNewValue, TError\u0026gt;\u0026gt; Map\u0026lt;TValue, TError, TNewValue\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, Func\u0026lt;TValue, ValueTask\u0026lt;TNewValue\u0026gt;\u0026gt; func) where TValue : notnull where TError : notnull where TNewValue : notnull // error CS0121: The call is ambiguous between the following methods or properties var dto = await user.Map(async u =\u0026gt; new UserDto(u.Id, await LoadAvatarUrl(u.Id))); C# 13 added [OverloadResolutionPriority], which assigns a score to methods. When several overloads apply, the compiler prefers the one with the highest score. Giving one of two ambiguous overloads a higher score resolves the ambiguity:\npublic static async Task\u0026lt;Result\u0026lt;TNewValue, TError\u0026gt;\u0026gt; Map\u0026lt;TValue, TError, TNewValue\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, Func\u0026lt;TValue, Task\u0026lt;TNewValue\u0026gt;\u0026gt; func) where TValue : notnull where TError : notnull where TNewValue : notnull [OverloadResolutionPriority(1)] public static async ValueTask\u0026lt;Result\u0026lt;TNewValue, TError\u0026gt;\u0026gt; Map\u0026lt;TValue, TError, TNewValue\u0026gt;( this Result\u0026lt;TValue, TError\u0026gt; result, Func\u0026lt;TValue, ValueTask\u0026lt;TNewValue\u0026gt;\u0026gt; func) where TValue : notnull where TError : notnull where TNewValue : notnull // CS0121 is missing. var dto = await user.Map(async u =\u0026gt; new UserDto(u.Id, await LoadAvatarUrl(u.Id))); In this library, the overload that gets the higher score depends on the type being extended:\nResult\u0026lt;TValue, TError\u0026gt;: the overload with a ValueTask delegate. Task\u0026lt;Result\u0026lt;TValue, TError\u0026gt;\u0026gt;: the overload with a Task delegate. ValueTask\u0026lt;Result\u0026lt;TValue, TError\u0026gt;\u0026gt;: the overload with a ValueTask delegate. Avoiding closures with extra arguments A library built around avoiding heap allocations has to be mindful of closures. A lambda that reads a variable from the enclosing scope has to carry it along, so the compiler generates a class to hold the variable and allocates an instance of it on the heap. Passing the variable in as an argument leaves nothing to carry:\n// captures factor, so a closure is allocated var captured = result.Map(x =\u0026gt; x * factor); // factor is an argument, so no closure is generated var passed = result.Map(static (x, factor) =\u0026gt; x * factor, factor); A library cannot stop callers from writing a capturing lambda. What it can do is give them a way to opt out, which is why every extension that takes a delegate accepts up to three extra arguments and passes them to the delegate.\nPutting it together With the extensions and their overloads in place, here is what changes in practice. Take a hypothetical user registration command handler. Some steps can fail. Others are asynchronous, meaning they are awaited, while the rest are synchronous. Written procedurally, every step that can fail needs a check before the next one runs:\npublic async ValueTask\u0026lt;Result\u0026lt;UserRegistered, string\u0026gt;\u0026gt; Handle(RegisterUserCommand command, CancellationToken ct) { if (!IsEmailValid(command.Email)) { return \u0026#34;Email is invalid\u0026#34;; } var free = await EnsureEmailIsFree(command, ct); if (free.Failure) { return free.Error; } var user = CreateUser(free.Value); var saved = await SaveUser(user, ct); if (saved.Failure) { return saved.Error; } await SendWelcomeEmail(saved.Value, ct); return ToResponse(saved.Value); } To chain the email check, ValidateEmail wraps the bool from IsEmailValid in a result that holds the command when the email is valid and an error when it is not:\nprivate static Result\u0026lt;RegisterUserCommand, string\u0026gt; ValidateEmail(RegisterUserCommand command) =\u0026gt; IsEmailValid(command.Email) ? command : \u0026#34;Email is invalid\u0026#34;; With the extensions, the branching is abstracted away and the same handler can be written as a single chain:\npublic ValueTask\u0026lt;Result\u0026lt;UserRegistered, string\u0026gt;\u0026gt; Handle(RegisterUserCommand command, CancellationToken ct) =\u0026gt; ValidateEmail(command) .Bind(EnsureEmailIsFree, ct) .Map(CreateUser) .Bind(SaveUser, ct) .Iter(SendWelcomeEmail, ct) .Map(ToResponse); EnsureEmailIsFree and SaveUser can fail, so they are chained with Bind. CreateUser and ToResponse cannot fail, so they are chained with Map. SendWelcomeEmail performs a side effect, so it is chained with Iter, which passes the result along unchanged. The chain treats asynchronous and synchronous steps the same way. ct goes in as an extra argument instead of being captured.\nConclusion The functional style is now available for Result\u0026lt;TValue, TError\u0026gt; as extension methods that reimplement F#’s Result functions. With the overloads worked out, chaining is easy, delegates can be passed as they are and async lambdas no longer cause ambiguity. Extra arguments let callers avoid closures when it matters. All of the extensions live in the same namespace, so callers never have to work out which namespace holds which overload just to chain a few calls. The result is a happy path that reads top to bottom without a check after every call. Further extensions are possible, but most of them can be built by combining the current ones.\nThe full source is on GitHub and the package is on NuGet.\n","permalink":"https://sharp-sheriff.xyz/posts/functional-extensions/","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eIn \u003ca href=\"/result-pattern-in-csharp\"\u003ean earlier post\u003c/a\u003e, I created \u003ccode\u003eResult\u0026lt;TValue, TError\u0026gt;\u003c/code\u003e, a generic, allocation-free struct. On its own, it supports only procedural programming. Call something, check \u003ccode\u003eSuccess\u003c/code\u003e or \u003ccode\u003eFailure\u003c/code\u003e, branch and repeat. This is the same boilerplate as Go’s \u003ccode\u003eif err != nil\u003c/code\u003e.\u003c/p\u003e\n\u003cp\u003eIn functional languages, a result is a value like any other, so operations can be composed on it directly. F# includes a \u003ccode\u003eResult\u003c/code\u003e module for this. This post discusses extension methods that reimplement \u003ccode\u003emap\u003c/code\u003e, \u003ccode\u003emapError\u003c/code\u003e, \u003ccode\u003ebind\u003c/code\u003e, \u003ccode\u003eiter\u003c/code\u003e, \u003ccode\u003eiterError\u003c/code\u003e, \u003ccode\u003edefaultValue\u003c/code\u003e and \u003ccode\u003edefaultWith\u003c/code\u003e for \u003ccode\u003eResult\u0026lt;TValue, TError\u0026gt;\u003c/code\u003e. They let you chain steps without checking \u003ccode\u003eSuccess\u003c/code\u003e or \u003ccode\u003eFailure\u003c/code\u003e after each call.\u003c/p\u003e","title":"Functional extensions for result"},{"content":"Introduction Whenever I was writing C# and an edge case popped up that strayed from the happy path, I just threw an exception. At first it felt fine and natural. But later it started bothering me, every new edge case meant adding another exception type to the error handling middleware. It was especially unpleasant when trying to compose different methods in order to reuse functionality. Exceptions aren\u0026rsquo;t cheap — throwing one measurably costs more than returning a value. Nor should they be relied on for control flow.\nI also tried programming in Go. At first I didn\u0026rsquo;t like how it dealt with errors — an if err != nil after every call felt like unnecessary boilerplate. But that boilerplate turned out to be the point, a function\u0026rsquo;s signature reflects whether it can fail and the error gets handled right there instead of somewhere else. It\u0026rsquo;s explicit instead of indirect, unlike exceptions — there\u0026rsquo;s no need to be aware of a try/catch elsewhere in the call stack. Naturally, I wanted that same explicitness in C#.\nBefore writing my own, I looked at what already existed for .NET. Most wrap everything in a class, so every success and failure allocates. Others predefine the error shape — a fixed enum or interface, often built to map straight onto an ASP.NET Core response, whether or not that\u0026rsquo;s what I needed. Some keep the value-returning and no-value variants apart but hide both behind a shared interface, which boxes the struct. Some ship an extensive set of functional utilities, most of which I don\u0026rsquo;t find useful. None of it is wrong — just not what I wanted.\nDesign and implementation The result pattern implementation needs to meet the following requirements:\nHold either a success value or an error, never both, never neither. Allow determining whether it contains a success value or an error. Be immutable. Be generic. Avoid heap allocations. With these requirements in mind, here\u0026rsquo;s a simple starting point:\npublic readonly struct Result\u0026lt;TValue, TError\u0026gt; { public Result(TValue value) { Value = value; Error = default; Success = true; } public Result(TError error) { Value = default; Error = error; Success = false; } public TValue? Value { get; } public TError? Error { get; } public bool Success { get; } public bool Failure =\u0026gt; !Success; } Struct layout Every result carries a field it will never use. The success case has an empty error slot, the failure case an empty value slot. Either way, part of the layout is dead space. To see this concretely, take Result\u0026lt;Guid, string\u0026gt; as an example:\n0 16 24 25 32 +------+----------+------+-----------+ | Guid | *string | flag | padding | +------+----------+------+-----------+ 32 bytes, of which at least 8 are guaranteed to be unused on every instance. Guid takes 16 bytes, *string takes 8, and they are never present at the same time — so a single 16-byte slot would hold either:\n0 16 17 24 +--------------------+------+-----------+ | max(Guid, *string) | flag | padding | +--------------------+------+-----------+ That\u0026rsquo;s 24 bytes, a quarter smaller, carrying exactly the same information.\nThere\u0026rsquo;s a way to get close to this. A single field typed object holds either case; the flag says which one is present, and Unsafe.As reads it back with no type check:\npublic readonly struct Result\u0026lt;TValue, TError\u0026gt; { private readonly object _value; public Result(TValue value) { _value = value; Success = true; } public Result(TError error) { _value = error; Success = false; } public TValue? Value =\u0026gt; Success ? Unsafe.As\u0026lt;TValue\u0026gt;(_value) : null; public TError? Error =\u0026gt; Failure ? Unsafe.As\u0026lt;TError\u0026gt;(_value) : null; public bool Success { get; } public bool Failure =\u0026gt; !Success; } This compiles and runs for any pair of type arguments. But anything that isn\u0026rsquo;t a reference type gets boxed on its way into the object field. A tighter layout that allocates on the heap isn\u0026rsquo;t a solution at all.\nLayoutKind.Explicit is meant for exactly this kind of overlap:\n[StructLayout(LayoutKind.Explicit)] struct Result\u0026lt;TSuccess, TError\u0026gt; { [FieldOffset(0)] private byte tag; [FieldOffset(1)] private TSuccess success; [FieldOffset(1)] private TError error; } // System.TypeLoadException: Could not load type \u0026#39;Result`2\u0026#39; from assembly \u0026#39;\u0026#39; // because generic types cannot have explicit layout. It compiles, because Roslyn treats FieldOffset as metadata. Explicit struct layout is not possible for generic structs due to a runtime restriction. Actual layout is only evaluated at runtime — which is where the TypeLoadException surfaces.\nThe layout could only be tightened by making it allocate, which is a bad trade. A fixed amount of unused memory per result is cheaper than the allocation and collection that would replace it.\nConstructor overload ambiguity The most obvious usage is also the one that breaks first: set TValue and TError to the same type.\n// error CS0121: The call is ambiguous between the following methods or properties var result = new Result\u0026lt;string, string\u0026gt;(\u0026#34;test\u0026#34;); The two constructors look different only because TValue and TError are still type parameters. Substitute the same type into both and they become indistinguishable. Constructors can\u0026rsquo;t be told apart by name. Fortunately, static methods don\u0026rsquo;t have that restriction:\nprivate Result(TValue value) { ... } private Result(TError error) { ... } public static Result\u0026lt;TValue, TError\u0026gt; Ok(TValue value) =\u0026gt; new(value); public static Result\u0026lt;TValue, TError\u0026gt; Fail(TError error) =\u0026gt; new(error); The constructors are still ambiguous, but they\u0026rsquo;re no longer reachable from outside. Ok builds a successful result and Fail builds a failed one. The compiler picks the method by name instead of leaving it to overload resolution:\n// CS0121 is missing. var ok = Result\u0026lt;string, string\u0026gt;.Ok(\u0026#34;test\u0026#34;); var fail = Result\u0026lt;string, string\u0026gt;.Fail(\u0026#34;test\u0026#34;); Nullability inference When a result is built, it\u0026rsquo;s clear whether it holds a value or an error. Checking the state flag later tells the compiler nothing:\nvar result = Result\u0026lt;string, string\u0026gt;.Ok(\u0026#34;test\u0026#34;); if (result.Success) { // CS8602: Possible dereference of null Console.WriteLine(result.Value.Length); } else { // CS8602: Possible dereference of null Console.WriteLine(result.Error.Length); } Value returns TValue? and Error returns TError?, so both are nullable. As far as flow analysis is concerned, Success and Failure are unrelated flags that happen to sit on the same struct. The framework provides the MemberNotNullWhen attribute for exactly this. It ties the state flags to the nullability of properties:\n[MemberNotNullWhen(true, nameof(Value))] [MemberNotNullWhen(false, nameof(Error))] public bool Success { get; } [MemberNotNullWhen(false, nameof(Value))] [MemberNotNullWhen(true, nameof(Error))] public bool Failure =\u0026gt; !Success; With the flags decorated, nullability in both branches is inferred correctly:\nvar result = Result\u0026lt;string, string\u0026gt;.Ok(\u0026#34;test\u0026#34;); if (result.Success) { // CS8602 is missing Console.WriteLine(result.Value.Length); } else { // CS8602 is missing Console.WriteLine(result.Error.Length); } Nullability override Right now, nullable types can be used for either TValue or TError. Combined with the nullability inference just added, that opens the door to a bug:\nvar result = Result\u0026lt;string?, int\u0026gt;.Ok(null); if (result.Success) { // inferred as non-null string type var value = result.Value; // System.NullReferenceException: Object reference not set to an instance of an object. Console.WriteLine(value.Length); } Ok(null) is legal, because TValue is string?. When Success is true, MemberNotNullWhen then overrides the compiler\u0026rsquo;s own analysis, so the Value property\u0026rsquo;s return type is treated as not null. The warning that would have caught this never appears, and reading value.Length throws a NullReferenceException.\nA nullable type argument quietly breaks the assumption the inference is built on. Constraining both type parameters keeps nullable type arguments out:\npublic readonly struct Result\u0026lt;TValue, TError\u0026gt; where TValue : notnull where TError : notnull { ... } // CS8714: The type \u0026#39;string?\u0026#39; cannot be used as type parameter \u0026#39;TValue\u0026#39; in the generic type or method. var result = Result\u0026lt;string?, int\u0026gt;.Ok(null); Implicit conversion Ok and Fail solved the ambiguity, but they left an ergonomic problem behind. A result is defined by two type parameters, and each creation method only ever sees one of them. Ok receives a TValue and has no way to know what TError should be. Fail has the same problem in reverse. C# has no partial type inference, so if one type argument can\u0026rsquo;t be inferred, both have to be written out:\npublic Result\u0026lt;User, ValidationError\u0026gt; Register(RegistrationRequest request) { if (!IsValidEmail(request.Email)) { return Result\u0026lt;User, ValidationError\u0026gt;.Fail(new ValidationError(\u0026#34;email\u0026#34;)); } return Result\u0026lt;User, ValidationError\u0026gt;.Ok(new User(request.Email)); } Implicit conversions solve it from the other direction. Instead of inferring the result type from the argument, they let the compiler take it from the target:\npublic static implicit operator Result\u0026lt;TValue, TError\u0026gt;(TValue value) =\u0026gt; new(value); public static implicit operator Result\u0026lt;TValue, TError\u0026gt;(TError error) =\u0026gt; new(error); Anywhere the target type is already known (returns, assignments, arguments), the conversion applies and the generic definition can be inferred:\npublic Result\u0026lt;User, ValidationError\u0026gt; Register(RegistrationRequest request) { if (!IsValidEmail(request.Email)) { return new ValidationError(\u0026#34;email\u0026#34;); } return new User(request.Email); } The drawback is the one from earlier. Substitute the same type into both operators and they become indistinguishable, exactly as the constructors did:\n// error CS0457: Ambiguous user defined conversions Result\u0026lt;string, string\u0026gt; result = \u0026#34;test\u0026#34;; The trick that saved the constructors isn\u0026rsquo;t available here. When both type arguments are the same, Ok and Fail remain the only way to build a result, so the full syntax has to be written out.\nRepresenting nothing Some operations can succeed or fail without having anything meaningful to report. Result\u0026lt;TValue, TError\u0026gt; still needs a TValue, and void isn\u0026rsquo;t a legal one:\n// error CS1547: Keyword \u0026#39;void\u0026#39; cannot be used in this context Result\u0026lt;void, string\u0026gt; result; What\u0026rsquo;s needed is a common type to stand in for \u0026ldquo;nothing,\u0026rdquo; but the usual ways to build one are both closed off here. Inheritance is out: Result\u0026lt;TValue, TError\u0026gt; is a struct, and a struct can\u0026rsquo;t inherit from a base class, only implement interfaces. An interface would satisfy TValue, except the moment a struct value is passed around through an interface reference it gets boxed — the exact solution this whole design exists to avoid.\nWhat\u0026rsquo;s left is a plain, standalone struct with nothing in it — a solution similar to MediatR\u0026rsquo;s own Unit, built for the same reason — something a notnull constraint will accept, whether standing in for TValue or TError:\npublic readonly struct Unit { public static Unit Value { get; } = default; } No instance fields, nothing to initialize per value — just a single static property handing out the one instance that exists. Every Unit is identical to every other Unit, which is the entire point — there\u0026rsquo;s exactly one possible value, so there\u0026rsquo;s nothing left to represent but \u0026ldquo;this happened.\u0026rdquo;\nConclusion All five requirements hold. Result\u0026lt;TValue, TError\u0026gt; can contain only a value or an error, Success and Failure say which one it has, the type itself is immutable and generic, none of that costs a heap allocation. The layout is the one thing I\u0026rsquo;d have liked tighter. TValue and TError still sit in two separate slots instead of one sized to the larger of the two. A real tagged union would need LayoutKind.Explicit, which isn\u0026rsquo;t allowed on generic structs, due to a runtime restriction, thus the layout stays suboptimal.\nThanks for reading this far. The full source is on GitHub, and the package is on NuGet. If it\u0026rsquo;s useful, or you just enjoyed reading how it came together, a star on the repo helps other people find it.\n","permalink":"https://sharp-sheriff.xyz/posts/result-pattern/","summary":"\u003ch2 id=\"introduction\"\u003eIntroduction\u003c/h2\u003e\n\u003cp\u003eWhenever I was writing C# and an edge case popped up that strayed from the happy path, I just threw an exception. At first it felt fine and natural. But later it started bothering me, every new edge case meant adding another exception type to the error handling middleware. It was especially unpleasant when trying to compose different methods in order to reuse functionality. Exceptions aren\u0026rsquo;t \u003ca href=\"https://dev.to/gramli/net-throwing-exceptions-vs-result-pattern-benchmark-4a62\"\u003echeap\u003c/a\u003e — throwing one measurably costs more than returning a value. Nor should they be relied on for \u003ca href=\"https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/exception-throwing\"\u003econtrol flow\u003c/a\u003e.\u003c/p\u003e","title":"Result Pattern in C#"}]