DEV Community

Noah
Noah

Posted on • Edited on

No more Try/Catch: a better way to handle errors in TypeScript

Hello everyone.

Have you ever felt that Try/Catch is a bit inconvenient when developing an application in TypeScript?

I found an intersting video on YouTube that describes how to handle errors in TypeScript in a simple way.
I'm sharing insights from the video as a review.

If you have any other good alternatives to try/catch, I would love to hear them!

Defining the getUser function for error handling

First of all, I defined a simple getUser function to illustrate error handling.
It returns a new user with the given id.

const wait = (duration: number) => {
  return new Promise((resolve) => {
    setTimeout(resolve, duration);
  });
};

const getUser = async (id: number) => {
  await wait(1000);

  if (id === 2) {
    throw new Error("404 - User does not exist");
  }

  return { id, name: "Noah" };
};

const user = await getUser(1);

console.log(user); // { id: 1, name: "Noah" }
Enter fullscreen mode Exit fullscreen mode

Error Handling using try/catch

Rewriting the previous code using try/catch, it looks like this.

const wait = (duration: number) => {
  ...
};

const getUser = async (id: number) => {
 ...
};

try {
  const user = await getUser(1);
  console.log(user); // { id: 1, name: "Noah" }
} catch (error) {
  console.log("There was an error");
}
Enter fullscreen mode Exit fullscreen mode

Problem with try/catch ①: It handles every error that occurs within the try block

The code below is not ideal.
Even though it's just a typo, "There was an error" is displayed in the console. I only want to handle errors that occur specifically in getUser within this try/catch block.

const wait = (duration: number) => {
  ...
};

const getUser = async (id: number) => {
 ...
};

try {
  const user = await getUser(1);
  console.log(usr);               // ← There was an error
  // ... (a lot of code)
} catch (error) {
  console.log("There was an error");
}
Enter fullscreen mode Exit fullscreen mode

Problem with try/catch ②: The Pitfall of Using let

Okay then, let's try to solve it using let.

const wait = (duration: number) => {
  ...
};

const getUser = async (id: number) => {
 ...
};

let user;

try {
  user = await getUser(1);
  // ... (a lot of code)
} catch (error) {
  console.log("There was an error");
}

console.log(usr); // ← ReferenceError: Can't find variable: usr
Enter fullscreen mode Exit fullscreen mode

I got an actual error from the typo, but this code is still not ideal because I can accidentally redefine the user object, like below.

const wait = (duration: number) => {
  ...
};

const getUser = async (id: number) => {
 ...
};

let user;

try {
  user = await getUser(1);
  // ... (a lot of code)
} catch (error) {
  console.log("There was an error");
}

user = 1 // ← ❌ It might lead to a bug.
Enter fullscreen mode Exit fullscreen mode

Solution

It's much simpler and more readable, don't you think?
Furthermore, the user variable is immutable and won't lead to unexpected errors.

const wait = (duration: number) => {
  ...
};

const getUser = async (id: number) => {
 ...
};

const catchError = async <T>(promise: Promise<T>): Promise<[undefined, T] | [Error]> => {
  return promise
    .then((data) => {
      return [undefined, data] as [undefined, T];
    })
    .catch((error) => {
      return [error];
    });
};

const [error, user] = await catchError(getUser(1));

if (error) {
  console.log(error);
}

console.log(user);
Enter fullscreen mode Exit fullscreen mode

Please take a look at the video, which we have referenced. He explains it very carefully.

I have never actually used this pattern in actual work.I just wanted to hear your opinion on how practical it is.Because it was discussed in his Yotube comments and I wanted to know the answer. I’ll be exploring best practices based on the comments.👍

Happy Coding☀️

Top comments (41)

Collapse
 
akashkava profile image
Akash Kava

It is bad design, it is like going back from exception to C style (error, result) returns.

If this pattern was useful, exceptions would never be invented.

When you nest your code, you need to rethrow the error as method has failed and result is undefined.

Such code will lead to nightmare as one will not be able to find out why result is invalid and error is not thrown. Console log doesn’t help for caller.

Exceptions were invented in such a way that top caller can catch it, log it and process it. There is no point in changing the calling sequence and rewrite same logic again to virtually invent new throw catch.

Collapse
 
benjamin_babik_8829e49cb3 profile image
Benjamin Babik

Errors and Exceptions are not the same thing. You don't have to like it, but typed errors is a very good discipline.

type Result<T, E> = { ok: true, value: T } | { ok: false, error: E }

type NetError =
  | { type: "offline" }
  | { type: "timeout" }

type GetError =
  | { type: "unauthenticated" }
  | { type: "unauthorized" }
  | NetError

type GetRecordResult = Result<Record, GetError>

function getRecord(id: string):Promise<GetRecordResult> {
  // ...
}

getRecord("too").then(r => {
  if (r.ok) {
    console.log(r.value)
  } else {
    // Now your IDE knows exactly what errors are returnable.
    // JavaScript exceptions can't do this...
    switch (r.type) {
      case "offline"
        notifyUserOffline();
        break;
      case:
        // ...
    }
  }
})
Enter fullscreen mode Exit fullscreen mode
Collapse
 
akashkava profile image
Akash Kava

I have had experience of following such pattern and it often leads to undetectable bugs.

If you don’t consider error and exception as same then you are inventing a third state, which in combination of other conditions, it just increases complexity in the flow of logic. And I had to undo every time I used this pattern.

Collapse
 
tombohub profile image
tombohub

that's state of the workflow, no need to be error type

Collapse
 
fsawady profile image
Federico Sawady

Finally a person on internet that says the truth behind the horror of bad error handling in Golang, and now Javascript and Typescript if people don't study well how to do good programming.

Collapse
 
rafaelassumpcao profile image
Rafael A

You as the majority of developers, think that good programming exist, when good programming is subjective to the person reading and writing it. So basically the ALL the programs you wrote are bad programming from the computer perspective, and might be bad for other programmers too. When you understand this, it is a life changing because you think less about good and bad to start thinking on patterns, which if is bad or good doesn't matter, just follow it blindly right? Wrong! Programming is a form of express yourself, that's teh beauty of programming, God damn! Stop bullying bad programmers bro

Collapse
 
squareguard profile image
Jackson Bayor

This is an interesting idea. But over my many years of coding, I find try catch very useful… some might disagree but I kinda see it as an advantage to other languages… maybe more like the panic in Go.

In my use cases, my apps never break, errors are handled elegantly, proper notifications are resolved… although, I use this pattern even in TS and haven’t come across any blockers.

But for sure I can try your idea and refine it but it would most likely complement the try catch syntax and advantages rather than replacing it.

Collapse
 
danielvandenberg95 profile image
Daniël van den Berg

I don't quite see the advantage of this over using

const user = await getUser(1).catch(console.log);

In what cases is your suggestion better?

Collapse
 
luke_waldren_7844872eb751 profile image
Luke Waldren

The advantage is more clear when you need to do more than console log the error. If you need to make a decision based on the error value, this pattern removes duplicated logic and enforces a consistent pattern in your code

Collapse
 
fsawady profile image
Info Comment hidden by post author - thread only accessible via permalink
Federico Sawady

A consistent pattern doesn't provide good properties to a program by itself. You could be doing assembly if that was the case.

Collapse
 
stretch0 profile image
Andrew McCallum • Edited

I like the concept. The syntax reminds me of Golang where it uses the ?=.

I do wonder about how much benefit this has since you now have to have 2 conditions where the error is handled, one in the catch block and one in the if block.

If you are wanting to suppress the error but still add a log of some sort, you can simply use a combination of await / catch

const user = await getUser().catch((err) => {
  console.log(err)
}) 
Enter fullscreen mode Exit fullscreen mode

This is less code and allows you to avoid using let

Edit:
Another approach I have recently come across is using Promise.allSettled which gives you the ability to use the rejected value inside an if block like so:

async () => {

  const myPromise = () => new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Hello, world!');
    }, 1000);
  })

  const  [result] = await Promise.allSettled([myPromise()]);

  if( result.status === 'rejected' ) {
    console.error('Error:', result.reason);
  }

  return result.value;
}
Enter fullscreen mode Exit fullscreen mode

I think this is really nice as it avoids using let and means you can avoid mixing syntaxes: async vs await with then / catch.

Collapse
 
noah-00 profile image
Noah

I basically agree with using await and .catch().
However, If you need to make a decision based on the error value, the syntax removes duplicated logic and enforces a consistent pattern in your code.

That said, I have never actually used this pattern in actual work.I just wanted to hear your opinion on how practical it is.
Thank you for your suggestion.👍

Collapse
 
levancho profile image
levancho

there is no problem with try catch and your suggested "solution" is actually looks more like a hack ... why not make typed catch ?? like it is in java that way you can catch exactly the error you need to .. is not typescript all
about being "typed" javascript anyways ???

Collapse
 
dm430 profile image
Devin Wall

Typescript doesn't handle any type of runtime typing. It's goal is simply to introduce static type checking to a code base.

What you're proposing would require some sort of runtime type differentiation. Which I suppose could work if all errors had a different type. Unfortunately that's likely not the case in most instances. To further compound the issue there isn't even a way to ensure the only thing being thrown is an error. Which is actually the reason the type in the catch block is of type unknown and not error.

With all of that said, using the result type pattern would likely be a better way to handle this. Localize the error handing, then return a result that indicates if the operation was successful.

Collapse
 
programmerraja profile image
Boopathi

This is a great breakdown of the limitations of try/catch and a more elegant solution for handling errors in TypeScript. The catchError function looks incredibly useful for maintaining clean and readable code.

Collapse
 
noah-00 profile image
Noah

@programmerraja
Thank you for the kind words! I’m glad you found the breakdown helpful. Yes, the catchErrorfunction is indeed a handy way to manage errors elegantly in TypeScript. It really makes a difference in keeping the codebase maintainable and easier to understand. If you have any thoughts or suggestions on improving the approach, I’d love to hear them!

Collapse
 
toriningen profile image
Tori Ningen

Instead of combining the worst of two, embrace Result type (also known as Either). Return your errors explicitly, handle them explicitly, don't throw exceptions.

Promise type is close to it, but unnecessarily combines Future with Result. Conceptually, both should have been independent, and Promise would've been Future> - e.g. a value that might eventually become a success or an error.

Then you'd await it to get Result, and pattern-match the Result to handle your error. It also removes the possibility of accidental error.

Collapse
 
abustamam profile image
Rasheed Bustamam

To add onto this, there are some TS libraries that implement this (and other algebraic types)

gcanti.github.io/fp-ts/modules/Eit...

effect.website/docs/data-types/eit...

Or you could roll your own:

type Success = { data: T, success: true }
type Err = { success: false, data: null, error: any } // can type the error if desired
type Result = Success | Err

async function tryPromise(prom: Promise): Result {
try {
return { success: true, data: await prom }
} catch (error) {
return { success: false, data: null, error }
}
}

ex:

const res = await tryPromise(...)

if (!res.success) // handle err
const data = res.data

Collapse
 
onepx profile image
Artemiy Vereshchinskiy

Open comments just to find this one. 🫶🏻

Collapse
 
toriningen profile image
Tori Ningen

I have just realized that dev.to ate the part of my message, confusing it for markup: "and Promise would've been Future>" should be read as "and Promise<T, E> would've been Future<Result<T, E>>".

Collapse
 
koehr profile image
Norman

So we're back at good old Nodejs style error handling? Although this might solve a few issues, it feels for me like it goes only half way in the right direction. While the article explains well some issues with try catch, it actually doesn't offer a solution and only a half-way mitigation, because in the end, instead of having no idea about which error hits youin the catch block, you now have no idea which error hit you in the first array element. I also think, using a tagged result object (like { ok: true, data: T } | { ok: false, error: E }) is a bit nicer here, as it offers some more room for extensions on the result object.

Collapse
 
crusty0gphr profile image
Harutyun Mardirossian

Great article. Thanks for elaborating on this topic. The try/catch format has limitations and introduces more clutter. I myself also faced the same problem in PHP. In my concept, I implemented Result type from Rust to return either an error or a value.

dev.to/crusty0gphr/resultt-e-type-...

Collapse
 
gopikrishna19 profile image
Gopikrishna Sathyamurthy • Edited

Personally, I am satisfied with native JS try/catch implementation over abstraction. However, yours feels like a step backward, remember the good old callbacks that got the error and response as arguments? Either way, I believe there are better ways to handle your problems:

1) Move the try/catch to the appropriate location, rather than a chain of try/catches, and let the control flow take care of things for you.

const getUser = async (id) => {
  try {
    const response = await fetch(`/user/${id}`)

    if (!response.ok) {
      return [new Error(`Failed to fetch user. got status: ${response.status}`), null]
      // new Error to create call stack
      // return, not throw
    }

    const user = await response.json();

    return [null, user];     // return, not assign
  } catch (error) {
    return [error, null];    // error!
  }
}

const [error, user] = await getUser(); // assign here
Enter fullscreen mode Exit fullscreen mode

2) Don't want to have response checks everywhere? I am not talking about the fetch specifically, but more of the pattern in general. It is better to move those further upstream with meaningful abstraction.

const jsonFetch = (...args) => {
  const response = await fetch(...args); // passthrough

  if (!response.ok) {
     throw new Error(`Failed to fetch user. got status: ${response.status}`);
     // why not return? I classify this as a system utility, and I would rather
     // keep it a consistent behaviour. So, no try/catch, no return, but throw.
  }

  return response.json();
}

const getUser = async (id) => {
  try {
    const user= await jsonFetch(`/user/${id}`);
    // simpler, meaningful, same as existing code
    return [null, user];
  } catch (error) {
    return [error, null];
  }
}
Enter fullscreen mode Exit fullscreen mode

3) Building on, we can also do more of a utilitarian approach (for the try/catch haters out there):

// const jsonFetch

const tuplefySuccess = (response) => [null, response];
const tuplefyError = (error) => [error, null];

const [error, user] = await getUser()
  .then(tuplefySuccess)
  .catch(tuplefyError);

// OR

const [error, user] = await getUser().then(tuplefySuccess, tuplefyError);

// OR

const tuplefy = [tuplefySuccess, tuplefyError];

const [error, user] = await getUser().then(...tuplefy); // TADA!
Enter fullscreen mode Exit fullscreen mode

4) Finally, modularize!

// json-fetch.js
export const jsonFetch = (...args) => { /* ... */ };  // abstraction of related code

// tuplefy.js
export const tuplefy = [...];  // utilities

// user.js
const getUser = (id) => jsonFetch(`/users/${id}`);
const [error, user] = await getUser(1).then(...tuplefy);   // Implementation
Enter fullscreen mode Exit fullscreen mode

With this, you are still following modern javascript sugars, meaningful abstraction, less cognitive overload, less error-prone, zero try/catch, etc. This is my code style. If you were on my team, you really don't want try/catch, and I was reviewing your PR for this utility, this is how I would have suggested you write the code. I'd greatly appreciate it if you could look at my suggestion as a pattern rather than an answer to the specific problems you presented. I welcome other devs to suggest different styles or improvements to my code as well. ❣️Javascript!

Some comments have been hidden by the post's author - find out more