Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
TypeScriptJavaScriptJune 22, 20264 min read

TypeScript Result Pattern: Replacing Exceptions with Discriminated Unions

TypeScript Result pattern implementation using discriminated unions allows you to handle errors explicitly, eliminating hidden runtime exceptions in your code.

TypeScriptError HandlingFunctional ProgrammingSoftware ArchitectureBest PracticesJavaScriptFrontend

During an on-call rotation last year, I spent three hours debugging a silent failure in a data processing pipeline. A deeply nested service was throwing a custom error that wasn't caught by the top-level handler, leaving our state in an inconsistent mess. I realized then that exception-driven control flow is a liability in complex systems. We needed a way to make failure a first-class citizen.

The solution was to stop relying on throw and start returning an explicit Result object. By leveraging TypeScript and discriminated unions, I was able to turn unpredictable runtime crashes into compile-time requirements.

Why Exceptions Fail You

In standard JavaScript, functions are "liars." A function signature like getUser(id: string): User implies it will return a user. But in reality, it might throw a DatabaseConnectionError, a UserNotFoundError, or a ValidationError.

Because these exceptions aren't part of the type signature, you often forget to catch them. Even if you use try/catch, you’re forced into a specific control flow that separates error handling from your business logic. It’s messy, and it’s hard to test.

Implementing the Result Pattern

To fix this, we define two types: Success and Failure. By using a shared "tag" property—the discriminator—TypeScript can narrow down exactly which state we're dealing with.

TYPESCRIPT
type Success<T> = {
  kind: CE9178">'success';
  data: T;
};

type Failure<E> = {
  kind: CE9178">'failure';
  error: E;
};

type Result<T, E> = Success<T> | Failure<E>;

This pattern is the foundation of functional error handling. Instead of throwing, your functions now explicitly declare that they might return an error.

Putting Discriminated Unions to Work

Let’s look at a concrete example. Imagine a function that fetches a user profile.

TYPESCRIPT
function fetchUser(id: string): Result<User, CE9178">'NOT_FOUND' | CE9178">'DB_ERROR'> {
  // Imagine some logic here
  if (dbDown) return { kind: CE9178">'failure', error: CE9178">'DB_ERROR' };
  if (!user) return { kind: CE9178">'failure', error: CE9178">'NOT_FOUND' };
  
  return { kind: CE9178">'success', data: user };
}

Now, when you call this function, you can't access data without checking the kind. If you try, TypeScript will throw a compiler error. This is a massive upgrade over try/catch, where you might accidentally access undefined or a partial object.

TYPESCRIPT
const result = fetchUser(CE9178">'123');

if (result.kind === CE9178">'failure') {
  console.error(result.error); // TypeScript knows exactly what this is
  return;
}

// Here, result is narrowed to Success<User>
console.log(result.data.name);

Moving Beyond Simple Returns

While this works for simple functions, it gets even better when you integrate it with other patterns. If you're building complex data pipelines, you might find that TypeScript Branded Types: Enforcing Domain Integrity at Compile-Time helps keep your Success payloads clean and validated.

However, the real power of the Result pattern comes when you start chaining operations. Writing manual if/else checks for every step becomes tedious. I’ve experimented with "pipe" functions that handle the unwrapping for you, but be careful—over-abstracting can make your code harder to read for junior teammates.

Trade-offs and Lessons Learned

I initially tried to implement a full-blown "Monad" library to handle these results. It was a mistake. My team struggled to understand the map and flatMap logic, and our onboarding time increased by about two days. We reverted to simple, explicit if/else checks using discriminated unions and it was much more maintainable.

If you're interested in applying these safety patterns to your infrastructure, check out how Next.js Server Actions: Implementing Type-Safe Mutations and Middleware uses similar validation strategies to keep mutations predictable.

Frequently Asked Questions

Does this replace all try/catch blocks? Not necessarily. Use the Result pattern for expected domain errors (e.g., "User not found," "Validation failed"). Keep try/catch for truly exceptional, unrecoverable system failures like network timeouts or out-of-memory errors.

Is this just functional programming? It's inspired by functional programming, but it's just standard TypeScript. You don't need to learn category theory to use it; you just need to understand how to narrow union types.

Does it add overhead? The runtime overhead is negligible. You're just returning an object instead of throwing an exception, which is often faster in V8 anyway. The real cost is in code verbosity, which is a fair trade for the reliability you gain.

I’m still refining my approach to handling nested results. Sometimes, the "pyramid of doom" returns, and I’m tempted to reach for a library again. But for now, keeping it explicit has kept my production code significantly more stable than it was a year ago.

Back to Blog

Similar Posts

TypeScriptJavaScriptJune 22, 20264 min read

TypeScript Configuration Patterns: Enforcing Type-Safe Partial Defaults

TypeScript configuration patterns help you build robust systems. Learn how to use key remapping and utility types to enforce partial defaults in your apps.

Read more
TypeScriptJavaScript
June 21, 2026
4 min read

TypeScript Dependency Injection: Clean Architectures Without Bloat

TypeScript Dependency Injection doesn't require heavy decorators. Learn how to use constructor overloading for a type-safe architecture that simplifies testing.

Read more
Vibrant fireworks illuminate the night sky over the Oberbaum Bridge in Berlin, capturing a festive cityscape.
TypeScriptJavaScriptJune 21, 20264 min read

TypeScript Event Emitters: Architecting Type-Safe Event Payloads

TypeScript Event Emitters are often brittle. Learn how to use interfaces and generics to enforce strict type safety and prevent runtime payload mismatches.

Read more