Home

Published

- 4 min read

Throwing exceptions kills apps. Here's how to fix that.

img of Throwing exceptions kills apps. Here's how to fix that.
Reach 7,100+ engaged devs by sponsoring this newsletter

My first programming language?

Turbo Pascal in high school.

It was fast, had good documentation, and could run on modest PC hardware.

The only problem with it?

It wasn’t built for large, complex software projects in mind. And so it was left behind as time went on.

The language itself is simple. But if you are not careful, due to its lack of modern features, you end up with a code that uses GoTo commands way too much.

And the next thing you know, the program execution jumps around more than a bunny in Duracell commercials.

In modern .NET development, you don’t see GoTo commands too often. However, there is one feature that can behave similarly:

exceptions.

If you throw an exception, it may be propagated to an entirely different layer up in the call stack. Or worse, not being handled at all. And cause runtime bugs and crashes.

Don’t get me wrong: exceptions are essential in modern .NET applications. But when you use them to control how your app should behave, instead of using them only for unexpected situations, you play a dangerous game.

The better alternative for control flow that signals both good and bad outcomes?

The Result pattern. Let’s dive a little bit to see what it is and how to use it.

What is the Result pattern, and what problem does it solve?

Let’s start with the problem.

In C#, when something goes wrong inside a method, you often throw an exception:

   public Customer GetCustomerById(Guid id)
{
    var customer = _dbContext.Customers.Find(id);
    if (customer == null)
    {
        throw new Exception("Customer not found");
    }
    return customer;
}

At first glance, it looks fine.

But what happens when the caller wants to handle that error?

They either need to wrap it in a try-catch, which quickly gets repetitive, or they miss it entirely. And boom, unhandled runtime crash.

(There is also an additional cost in throwing and catching exceptions compared to regular control flow, but in smaller apps, this is negligible.)

But here is where the Result pattern comes in.

Instead of throwing an exception, you return an object that indicates either a success or a failure:

   public Result<Customer> GetCustomerById(Guid id)
{
    var customer = _dbContext.Customers.Find(id);
    if (customer == null)
    {
        return Result.Fail<Customer>("Customer not found");
    }
    return Result.Ok(customer);
}

Now, the caller must explicitly check the result:

   var result = service.GetCustomerById(id);
if (result.IsFailure)
{
    // Handle the error
    Console.WriteLine(result.Error);
}
else
{
    var customer = result.Value;
    // Continue with happy path
}

No hidden surprises. No unexpected jumps in your program flow. Just clear, predictable handling of success and failure.

Limitations of the Result pattern

First, returning a Result from every method can be tedious.

Especially when errors need to propagate deep inside your codebase.

If your app has too many layers, you may end up writing code like this:

   if (!result.IsSuccess)
{
    return Result.Fail(result.Error);
}

Second, most C# developers aren’t trained to think this way. The majority of them use exceptions to propagate all errors in the app. So, if you want to introduce a Result pattern, be prepared for some raised eyebrows and pushback.

Third, the pattern isn’t built into C#.

At least, not yet.

There is a proposal to add discriminated unions (think: Result built into the language) in a future C# release, but it’s hard to know when this will end up inside C#.

But until then, you have to roll your Result type.

Or use one of the existing NuGet packages.

In the long run, how you handle errors will shape the stability of your application.

  • You can keep relying on exceptions for everyday control flow.
  • You can keep hoping you catch every edge case.
  • You can keep fixing bugs after they cause crashes.

Or you use the Result pattern.

  • Start predicting error flows in your code.
  • Start making success and failure explicit.

Good developers focus just on happy paths.
Great developers design for when things go wrong.

If you're a .NET developer who's looking to level up your career, but struggles to keep up with the latest cool stuff in .NET...

Every Friday I share actionable .NET advice, insights, and tips to learn the latest .NET concepts, frameworks, and libraries inside my FREE newsletter.

Join here 7,100+ other developers