DEV Community

Cover image for I Stopped Using Try-Catch in TypeScript and You Should Too

I Stopped Using Try-Catch in TypeScript and You Should Too

Shayan on June 21, 2025

I'm about to share something that might get me canceled by the JavaScript community: I've stopped throwing errors in TypeScript, and I'm never goin...
Collapse
 
lemii_ profile image
Lemmi

Honestly the result & option patterns are the best patterns I’ve seen in so long. They just make more sense but so many people will be stuck in their ways.

Collapse
 
dariomannu profile image
Dario Mannu

they may need lots of byte-sized examples they can copy, absorb and replicate plus a bit of time...

Collapse
 
vince_coppola_cc0f46e2439 profile image
Vince Coppola

Love it. This is actually a popular/modern practice and has first-class support in several other languages, e.g. C++'s std::expected and Rust's Result 💯

Collapse
 
dinakajoy profile image
Odinaka Joy

Even OCaml which made me love statically-typed languages more. Great post

Collapse
 
shayy profile image
Shayan

Yeah i wish it was the default in JS xD

Collapse
 
code42cate profile image
Jonas Scholz

🍿

Collapse
 
john_p_wilson profile image
John Wilson

Yeah, after spending some time with Go/Rust, I've started to use the same pattern in TypeScript. It is generally much more verbose and stable, and for common error types I create a dedicated error handlers, which are type-safe too. Overall, this is really nice solution.

Collapse
 
kurealnum profile image
Oscar

This makes me very grateful for how Rust handles errors. Most of what you just described comes out of the box in Rust.

Collapse
 
kristofer_meetvista profile image
Kristofer

What if we implement custom errors and throw them, extend basic error? Our custom error can implement the error code and we can still use throw / catch?

Collapse
 
xwero profile image
david duymelinck

Lets take in account the "Problem with throwing" section, and rewrite the code.

class UserNotFound extends Error {
  constructor(message: string = 'User not found') {
    super(message);
    this.name = 'UserNotFound';
  }
}

class UserNotActive extends Error {
  constructor(message: string = 'User is not active') {
    super(message);
    this.name = 'UserNotActive';
  }
}

async function getUser(id: string): Promise<User | UserNotFound | UserNotActive> {
  const user = await db.query(`SELECT * FROM users WHERE id = ?`, [id]);
  if (!user) {
    return new UserNotFound();
  }
  if (!user.isActive) {
    return new UserNotActive();
  }
  return user;
}
Enter fullscreen mode Exit fullscreen mode
  1. The function signature is not lying to you anymore
  2. You know what output types to expect
  3. You are forced to check the output type
  4. The errors have context

The extra benefit is that the errors are checked on class name instead of on a string. This reduces errors by typos.

The main driver in the Result solution is that instead of throwing the errors they are returned. So you don't need the type to make the solution happen.