ZyVOP Logo
Content That Connects
SeriesAI NewsWhy ZyVOPJoin Discord
LoginGet Started
ZyVOP Logo
Content That Connects

The Developer Publishing Hub. Write once, cross-post to Dev.to, Medium, Hashnode, WordPress & Bluesky with automated canonical source tags and zero paywalls.

Content

  • Categories
  • Tags
  • Badges
  • Leaderboard
  • Write Article
  • Newsletter

Company

  • About Us
  • Why ZyVOP
  • Developer API & CLI
  • Write for Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Developer Publishing Hub.

Zero paywalls · Full content ownership
All systems operational
HomeThe Death of Try/Catch: A Better Way to Handle Errors in TypeScript

The Death of Try/Catch: A Better Way to Handle Errors in TypeScript

Modern Error Handling Patterns for Safer, Cleaner, and More Predictable TypeScript Applications

ZyVOP
ZyVOP
Senior Developer
May 22, 2026
3 min read
The Death of Try/Catch: A Better Way to Handle Errors in TypeScript
#react#TypeScript#Architecture#try/catch#Error Handling#Best Practices

If you write JavaScript or TypeScript, your asynchronous code probably looks like a massive nesting doll of try/catch blocks.

async function fetchUserData(userId: string) {
  try {
    const user = await db.users.find(userId);
    try {
      const posts = await api.fetchPosts(user.id);
      return { user, posts };
    } catch (apiError) {
      console.error("Failed to fetch posts");
      throw new Error("Post fetching failed");
    }
  } catch (dbError) {
    console.error("Failed to fetch user");
    throw new Error("Database failed");
  }
}

This pattern is deeply flawed for three reasons:

  1. Scope leakage: Variables defined inside try cannot be accessed outside of it without declaring them as let beforehand.

  2. Loss of Type Safety: In TypeScript, errors caught in a catch(error) block are always typed as unknown or any. You have no idea what actually failed.

  3. The "Throw everything" antipattern: We use errors for normal control flow, making it impossible to know if a function will actually return a value or blow up the call stack.

Let's look at a much better, highly practical way to handle errors in TypeScript.


The Golang Approach: Errors as Values

In languages like Go and Rust, errors are not thrown into the void; they are returned as standard values. We can easily adopt this pattern in TypeScript.

Instead of returning Data and throwing an Error, we return a tuple: [Error | null, Data | null].

Creating the Wrapper

First, let's create a tiny utility function that wraps any Promise:

// utils/catchAsync.ts

type SafeReturn<T, E = Error> = 
  | [E, null]
  | [null, T];

export async function catchAsync<T, E = Error>(
  promise: Promise<T>
): Promise<SafeReturn<T, E>> {
  try {
    const data = await promise;
    return [null, data];
  } catch (error) {
    return [error as E, null];
  }
}

Refactoring our Code

Now, let's rewrite our nested try/catch disaster using our new catchAsync utility.

import { catchAsync } from './utils/catchAsync';

async function fetchUserData(userId: string) {
  // 1. Fetch User
  const [userError, user] = await catchAsync(db.users.find(userId));
  
  if (userError) {
    console.error("Database failed", userError);
    return null; // Handle it gracefully, right here.
  }

  // 2. Fetch Posts (we know 'user' is safely defined here)
  const [postError, posts] = await catchAsync(api.fetchPosts(user.id));
  
  if (postError) {
    console.error("API failed", postError);
    // Maybe we just return the user with empty posts if the API is down
    return { user, posts: [] }; 
  }

  // 3. Success
  return { user, posts };
}

Why is this vastly superior?

  1. Straight-line Code: We've completely eliminated nesting. The code reads cleanly from top-to-bottom.

  2. Const correctness: We can use const for user and posts because they are instantiated in the main scope, not trapped inside a try block.

  3. Forced Error Handling: Because you must destructure the error [error, data], TypeScript practically forces you to acknowledge that an error might happen before using the data.

  4. Type Narrowing: When you check if (error), TypeScript automatically narrows the type of data to exactly T in the rest of the function block. No more unknown.

Advanced: The Result Pattern (NeverThrow)

If you want to take this to the absolute limit of functional type safety, you can use the Result pattern (heavily inspired by Rust), often implemented via libraries like neverthrow.

npm install neverthrow

Using neverthrow, functions explicitly return a Result object that is either ok or err.

import { Result, ok, err } from 'neverthrow';

// Function explicitly declares it returns a User OR a DatabaseError
function getUser(id: string): Result<User, DatabaseError> {
  const user = db.find(id);
  
  if (!user) {
    return err(new DatabaseError("User not found"));
  }
  
  return ok(user);
}

const result = getUser("123");

if (result.isErr()) {
  // TypeScript knows result.error is exactly 'DatabaseError'
  console.log(result.error.message);
} else {
  // TypeScript knows result.value is exactly 'User'
  console.log(result.value.name);
}

Conclusion

Stop using try/catch for expected application logic (like an API returning a 404, or a database query finding no records). Reserve throw for truly exceptional, catastrophic failures (like running out of memory or infinite loops).

By treating errors as standard return values, your code becomes highly predictable, far easier to test, and perfectly typed.

Comments (0)

Login to post a comment.

ZyVOP
ZyVOP

Founder of Zyvop 🚀 | Building AI-driven tools & premium insights for software engineers, CTOs, and tech leaders. Obsessed with automating workflows and exploring the frontier of AI.

Subscribe to ZyVOP's Newsletter

More from ZyVOP

View profile

Debian Adopts "Responsible Use of Generative AI" After Nine-Way Condorcet Vote

Debian's General Resolution 2026-002 closed on August 28 with "Responsible Use of Generative AI" beating eight rival proposals, including a Social Contract ban, by a clear Condorcet margin, per the project secretary's published beat matrix.

3 minAug 30

How I Built a Real-Time Developer Trend Radar Into My SEO Growth Engine

An AI-powered content intelligence system that streams live developer conversations from Hacker News, Dev.to, Google Search, and GitHub — and turns them into ready-to-write blog opportunities with one click.

12 minAug 29

Qwen3.8-Flash-Next Cost Efficiency, OpenExecutive Satire, and Multi-Vector Retrieval Advances

This week's digest covers Qwen3.8-Flash-Next's push for ultimate cost-efficiency, the viral OpenExecutive project, and the technical release of MultiVectorEncoder in Sentence-Transformers v6.0.

4 minAug 28

Anthropic’s Pricing Shock, Granite 4.2 Open‑Source Leap, and AI‑Powered Security & Policy Shifts

From Anthropic’s flagship model losing steam to IBM’s 512 K‑token Granite 4.2, plus a new wave of AI‑driven security exploits and policy alarms, this week’s digest maps the technical and market forces you need to act on now.

3 minAug 26

Introducing Questions and Discussions: A New Way to Connect!

We are thrilled to announce a major update to how you can interact and share content on our platform! Up until now, sharing your thoughts meant writing a standa...

2 minAug 1