Ir para o conteúdo principal

← todas as notas

📎 Webclip

Replacing Exceptions-as-flow-control with the Result Pattern

Andrew Lock opens the first post of a series on the result pattern by responding to a public complaint from Jeremy Miller, who has been recommending people rip the pattern out of their codebases. Lock’s counter isn’t a full defense: he grants that Result objects threaded back through mediator handlers into MVC can pile on abstraction, but argues the core benefits still hold. Exceptions are expensive to throw in .NET, so using them for routine control flow is costly, and a method that returns Result instead of T makes its failure conditions visible in the signature instead of hidden in undocumented throws.

The post is a worked refactor of a hypothetical UserProvisioningService, walked through three stages to make the trade-offs concrete. The happy-path version hides its failure modes entirely: nothing signals what happens if claim validation comes back empty or tenant lookup fails. Adding exceptions for each failure case fixes that but introduces its own cost, verbose try/catch wrapping and exception types the caller has no way to discover from the method signature alone. Replacing those exceptions with a basic Result class (an IsSuccess flag plus Value or Error) makes failure explicit, but Lock’s first version still lets you access Value or Error incorrectly, and only avoids that at the cost of a Switch()-based version whose nested callbacks produce a “pyramid of doom” that’s harder to read than either of the two versions it replaced.

Fichamento
#

  • Lock’s stated case for the result pattern: exceptions are performance-expensive as ordinary control flow in .NET, and returning Result instead of T makes failure conditions explicit in the method signature rather than hidden in undocumented throws.
  • He acknowledges Miller’s specific complaint (Result objects threaded through mediator handlers into MVC, adding abstraction) without disputing it directly, framing his own argument as about the pattern’s core benefit rather than every implementation of it.
  • Stage 1 (happy path only): the example service assumes every step succeeds; nothing in the code signals what should happen if claim validation returns nothing or tenant lookup fails.
  • Stage 2 (exceptions for flow control): throwing typed exceptions (ValidationException, UnknownTenantException) at each failure point works but is expensive at runtime, requires the caller to know which exceptions to catch since the method signature doesn’t declare them, and gets verbose fast if you need to wrap each call to produce a “correct” semantic exception type.
  • Stage 3 (basic Result): a class with IsSuccess, Value, and Error properties removes the exception cost and makes failure visible in the return type, but nothing stops calling code from reading Value or Error on the wrong branch, a bug that surfaces as a real NullReferenceException.
  • Stage 4 (safer Result via Switch()): hiding Value/Error as private fields and forcing access through a Switch(onSuccess, onFailure) method closes that hole, but chaining several Switch() calls produces deeply nested callbacks, the “pyramid of doom”, that Lock calls “kind of horrible” to read.
  • Lock’s own conclusion for this post: none of the three stages shown is the version he’d recommend using. The safer Result fixes the type-safety problem but at a verbosity cost he flags as unsustainable, deferring the actual fix (LINQ-based extensions on Result) to the next post in the series.