DEV Community

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

Noah on November 04, 2024

Hello everyone. Have you ever felt that Try/Catch is a bit inconvenient when developing an application in TypeScript? I found an intersting video...
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!

Collapse
 
karega profile image
Karega McCoy

You could write the same code and still use a try/catch.

Collapse
 
dhananjayve profile image
Dhananjay verma

Inspired by golang error handling 😀👍

Collapse
 
noah-00 profile image
Noah

@dhananjayve
I saw that mentioned in his YouTube comments as well. I haven’t used Golang before, but I’ll look into it. Thank you!

Collapse
 
prakhart111 profile image
Prakhar Tandon

Syntax is good. I found it similar to how RTK Query has structured their data fetching. Same "function" gives both data, error and a loading state (you can add a loading state here too, if the given operation takes some time)

Collapse
 
janjakubnanista profile image
Ján Jakub Naništa

Nice! In fact this approach is the basis of TaskEither in fp-ts package, check it out - indispensable!

Collapse
 
artoodeeto profile image
aRtoo • Edited

This looks like go error handling works. Returns a tuple for error and value.

You still have to type check the error here tho. Your syntax error is a bad example.

You can also wrap the try catch in a function that can throw error or return values and can catch each type.

Collapse
 
louis7 profile image
Louis Liu

Hmm.. you need to call catchError every time you request data from the remote? It seems not graceful. This kind of error-handling logic should be put into the HTTP request service rather than in your business logic.

Collapse
 
pierrejoubert73 profile image
Pierre Joubert • Edited

OP shows innovative thinking and tries new things. Props for that. This is the only way new and better ways are discovered!

Before my sincere reply, first some fun:

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

No hahaha

Jokes aside, this approach has the potential to make things a bit more graceful, but nothing that can be accomplished with more traditional means.

The price you pay for the new error handling is offloading it onto how you interact with your code. It makes things a bit more convoluted and more complex to read.

const [error, user] = await catchError(getUser(1));
Enter fullscreen mode Exit fullscreen mode

vs

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

Perhaps it makes more sense in your specific case. However, this exercise is somewhat unnecessary in a RESTful or Handler-oriented environment, such as Fastify.

Extra thought: Could you offload conditional error handling to the catch block? It's not as graceful, but it won't interfere with your invocation.

...
} catch (error) {
  // if error.message do x
}
Enter fullscreen mode Exit fullscreen mode

Sorry, I accidentally submitted the comment and had to edit it quickly.

Collapse
 
moseskarunia profile image
Moses Karunia

Since a lot already shows up the pros of using this approach, I'll try to provide the cons.

This is good if we are not doing any kind of error management, which is transforming different responses from different backends into the app's error structure.

I once does this kind of error handling, using generics, just in different language, eventually, i think the most readable way is to not abstract out simple logic like this.

Instead, if you encapsulate each function / use case of the app in a single function, you can have some kind of independence in each of them, leading to better readability.

My approach sacrifices DRY, I'm aware of it, but I think being too DRY can also impact readability in some ways.

Readability as in "when I need to understand this logic, I need to read multiple different logics that the original logic might depend on".

Collapse
 
bluebaroncoder profile image
James

Don't do error handling locally. Let the error flow through to be caught by a universal error handler. Especially in .NET where you can have a middleware handle errors. You shouldn't endeavor to write error ceremony every time you want to make a call.

Collapse
 
mrdulin profile image
official_dulin

Go style error handling:

func Open(name string) (file *File, err error)
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ajjaana profile image
X Daniel

This is a horrible approach. You are literally using try catch, it is even in your final code. Also you are wrapping your code into an async task with all the additional objects which is quite an overhead. Also, as a side effect you are using exception handling for a kind-of flow control which is a big no-no, antipattern.

Collapse
 
sklavit profile image
Sergii Nechuiviter

It doesn’t improve much callback hell case.

Collapse
 
alexdev404 profile image
Immanuel Garcia

If you're decent enough to be able to write good code, you'll never need such a workaround.

Collapse
 
eskalonad profile image
Eskalona-Lopes Denys

Looks like golang. But from my point of view golang way is better because utility values are placed after main one.
We've tried that approach combined with named tuples on our project, i like it.

Collapse
 
hacker_02 profile image
Havker

But i think i will stick with try/catch block 😂

Collapse
 
erezamihud profile image
ErezAmihud

This is called a "monad" - a concept from functional programming where the function return error or result

Collapse
 
xs217_b369f4e51478fc772f9 profile image
John

I do something similar. I recommend though that you use null instead of undefined as your non-error. Undefined should be avoided unless you have a good reason (i.e. an edge-case reason) to use it.

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