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

Noah @noah-00

About: Frontend / Full Stack Developer | TypeScript | React | Next.js | Node.js | AWS

Location:
Canada, Vancouver
Joined:
Jun 15, 2024

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

Publish Date: Nov 4 '24
298 41

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☀️

Comments 41 total

  • Boopathi
    BoopathiNov 5, 2024

    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.

    • Noah
      NoahNov 5, 2024

      @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!

  • Dhananjay verma
    Dhananjay vermaNov 5, 2024

    Inspired by golang error handling 😀👍

    • Noah
      NoahNov 5, 2024

      @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!

  • Daniël van den Berg
    Daniël van den BergNov 5, 2024

    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?

    • Luke Waldren
      Luke WaldrenNov 5, 2024

      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

  • Andrew McCallum
    Andrew McCallumNov 5, 2024

    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.

    • Noah
      NoahNov 6, 2024

      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.👍

  • Karega McCoy
    Karega McCoyNov 6, 2024

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

  • Tori Ningen
    Tori NingenNov 6, 2024

    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.

    • Rasheed Bustamam
      Rasheed BustamamNov 6, 2024

      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

    • Tori Ningen
      Tori NingenNov 6, 2024

      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>>".

  • Prakhar Tandon
    Prakhar TandonNov 6, 2024

    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)

  • aRtoo
    aRtooNov 6, 2024

    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.

  • Sergii Nechuiviter
    Sergii NechuiviterNov 6, 2024

    It doesn’t improve much callback hell case.

  • levancho
    levanchoNov 6, 2024

    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 ???

    • Devin Wall
      Devin WallNov 7, 2024

      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.

  • Jackson Bayor
    Jackson BayorNov 6, 2024

    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.

  • Moses Karunia
    Moses KaruniaNov 6, 2024

    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".

  • Gopikrishna Sathyamurthy
    Gopikrishna SathyamurthyNov 6, 2024

    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!

  • Harutyun Mardirossian
    Harutyun MardirossianNov 6, 2024

    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-...

  • Norman
    NormanNov 6, 2024

    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.

  • Akash Kava
    Akash KavaNov 6, 2024

    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.

    • Federico Sawady
      Federico SawadyNov 7, 2024

      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.

      • Rafael A
        Rafael ANov 7, 2024

        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

    • Benjamin Babik
      Benjamin BabikNov 7, 2024

      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
      • Akash Kava
        Akash KavaNov 8, 2024

        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.

      • tombohub
        tombohubNov 10, 2024

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

  • Havker
    HavkerNov 6, 2024

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

  • Pierre Joubert
    Pierre JoubertNov 6, 2024

    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.

  • Ján Jakub Naništa
    Ján Jakub NaništaNov 6, 2024

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

  • John
    JohnNov 6, 2024

    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.

  • James
    JamesNov 7, 2024

    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.

  • X Daniel
    X DanielNov 7, 2024

    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.

  • ErezAmihud
    ErezAmihudNov 7, 2024

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

  • Immanuel Garcia
    Immanuel GarciaNov 7, 2024

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

  • Louis Liu
    Louis LiuNov 7, 2024

    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.

  • Eskalona-Lopes Denys
    Eskalona-Lopes DenysNov 11, 2024

    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.

  • official_dulin
    official_dulinDec 4, 2024

    Go style error handling:

    func Open(name string) (file *File, err error)
    
    Enter fullscreen mode Exit fullscreen mode
Add comment