DEV Community

YCM Jason
YCM Jason

Posted on • Edited on

TRY {} CATCH {} โ€’ FIXED! (with a surprisingly old trick)

This article is loosely based off my recent video:

Hey everyone, Jason here ๐Ÿ‘‹

Let's talk about something that many JavaScript developers love to hate: try-catch.

In this article, I want to explore:

  1. why try-catch can be frustrating
  2. discuss a common solution many have proposed, and
  3. introduce a pattern that I haven't seen enough people talk about

The Problem with try-catch

Consider this function:

function getUserAndPreference() {
  try {
    const user = getUser();
    const preference = getUserPreference(user);
    return [user, preference];
  } catch (error) {
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

At first glance, this seems fine. But here's the issue: the try block catches all errors within it. That means if getUser or getUserPreference throws an error, or even if there's a typo elsewhere, they all get caught in the same catch block. This makes debugging and maintenance harder as your function grows.

To handle errors more granularly, you might consider wrapping each operation in its own try-catch:

function getUserAndPreference() {
  let user: User;
  try {
    user = getUser();
  } catch (error) {
    return null;
  }

  let preference: UserPreference;
  try {
    preference = getUserPreference(user);
  } catch (error) {
    return null;
  }

  return [user, preference];
}
Enter fullscreen mode Exit fullscreen mode

But this pattern has its own issues:

  1. Forced to use let instead of const This pattern requires us to declare the variables and assign them in different scope. This forces us to replace our "supposedly" const to let.
  2. Forced to use explicit typing In the previous example, all variables are typed automatically thanks to TypeScript's type inference. However, this approach forces us to seperate the declaring and assigning of the variables, meaning that TypeScript couldn't determine the variables types at declaration and use implicit any for those variables!

To summarize, the 3 pain-points of try-catch are:

  1. Catch-all behaviour
  2. Forcing let instead of const
  3. Compromising type inference by TypeScript

The "Do Not Throw" Pattern

To address these issues, many developers advocate for a "do not throw" approach. Instead of throwing errors, functions return a tuple containing the error and the result.

Here's a really simple example of such concept:

const tryCatch = async <T>(
  promise: Promise<T>,
): Promise<[error: null, result: T] | [error: Error]> => {
  try {
    return [null, await promise];
  } catch (error) {
    return [error];
  }
};

Enter fullscreen mode Exit fullscreen mode

Usage:

const [error, user] = await tryCatch(getUser());
if (error) {
  // handle error
}
Enter fullscreen mode Exit fullscreen mode

This pattern mitigates all 3 issues mentioned in the previous section: allows for more granular error handling, allows for using const, and still keep TypeScript's type inference.

However, I find it not to be the most idiomatic way to JavaScript (handling errors as return values) and requires a utility function that's not standardized which causes never-ending discussions amongst engineers with different background and preferences.

Enter IIFE: Immediately Invoked Function Expression

Image description

IIFE, pronounced as Eevee (the Pokรฉmon), is a pattern that's been around since the earliest days of JavaScript.

const user = await (async () => {
  try {
    return await getUser();
  } catch (error) {
    return null;
  }
})();
Enter fullscreen mode Exit fullscreen mode

By wrapping the operation in an IIFE, we can use try-catch as usual while eliminating all problems with try-catch mentioned previously.

We can apply the same pattern to getUserPreference:

const preference = await (async () => {
  try {
    return await getUserPreference(user);
  } catch (error) {
    return null;
  }
})();
Enter fullscreen mode Exit fullscreen mode

Now, our function looks like this:

async function getUserAndPreference() {
  const user = await (async () => {
    try {
      return await getUser();
    } catch (error) {
      return null;
    }
  })();

  if (!user) return null;

  const preference = await (async () => {
    try {
      return await getUserPreference(user);
    } catch (error) {
      return null;
    }
  })();

  if (!preference) return null;

  return [user, preference];
}
Enter fullscreen mode Exit fullscreen mode

This approach allows for granular error handling, maintains code integrity, and leverages type inferenceโ€”all without any utility functions.

Looking Ahead: Do Expressions

There's a proposal for "do expressions" in JavaScript, currently at stage 1. It aims to bring similar functionality as IIFE but with cleaner syntax:

const getUserAndPreference = async () => {
  const user = do {
    try {
      getUser();
    } catch (error) {
      // handle errors here
      return null; // return from getUserAndPreference
    }
  };
  // ...
};
Enter fullscreen mode Exit fullscreen mode

Once this proposal is accepted as the language standard, try-catch IIFEs can migrate seamlessly to do expressions.

Final Thoughts

try-catch isn't inherently evil. It's how we use it that matters. By leveraging IIFE, we can write cleaner, more maintainable code without relying on external utilities or compromising code integrity.

I hope you found this helpful! If you did, please like, give me a follow, and share with your friends. If you have any questions or ideas, feel free to comment and let me know.

Let me know if you'd like to explore more patterns or have any questions!

Top comments (6)

Collapse
 
moopet profile image
Ben Sinclair

All of this is fine and does solve the problems you're talking about, but at the expense of readability. There's more visual clutter and cognitive load deciphering what multiple complex blocks mean (even with the potential do).

Collapse
 
ycmjason profile image
YCM Jason

I totally see where you're coming from!

IIFE can feel unfamiliar or visually โ€œheavyโ€ at first โ€” especially if you're not used to wrapping async logic like that. But once it clicks, I actually find it declutters code by grouping logical units together.

Plus, IIFE has so many more use cases beyond just try-catch. My next video/article is probably going to dive deeper into it โ€” I honestly think IIFE is super underrated in the JS community, and I really want to advocate for it more.

That said, itโ€™s all down to personal preference in the end. The best approach is whatever works for you! ๐Ÿ˜€ All I ask is to keep an open mind and try it out next time โ€” let me know what you think!

Curious โ€” would you lean toward the [err, result] tuple pattern or similar "do not throw" patterns instead?

Collapse
 
nevodavid profile image
Nevo David

honestly i always end up wrestling with try-catch so seeing this iife trick feels like a shortcut - you think swapping little patterns like this really ends up changing how we build bigger stuff down the line or nah

Collapse
 
ycmjason profile image
YCM Jason

Yeah, I think little patterns like this can make a difference.

IIFE groups logic cleanly โ€” especially when itโ€™s a one-off that doesnโ€™t need a name. It keeps things local and avoids the weird side effects of broad try-catch blocks.

If it ever feels messy, you can always extract it into a function and give it a name. I usually only do that when I actually need to reuse the error handling. (Which is also a nice way to reuse logic without abstracting too early, you know, naming is hard.)

Curious โ€” have you used IIFE in your projects before, or is this your first time seeing it used like this?

Collapse
 
michael_liang_0208 profile image
Michael Liang

Nice post!

Collapse
 
ycmjason profile image
YCM Jason

thanks man!