Skip to content

Instantly share code, notes, and snippets.

@karlhorky
Last active October 19, 2022 23:43
Show Gist options
  • Star 17 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save karlhorky/3593d8cd9779cf9313f9852c59260642 to your computer and use it in GitHub Desktop.
Save karlhorky/3593d8cd9779cf9313f9852c59260642 to your computer and use it in GitHub Desktop.
Try-catch helper for promises and async/await
export default async function tryCatch<Data>(
promise: Promise<Data>,
): Promise<{ error: Error } | { data: Data }> {
try {
return { data: await promise };
} catch (error) {
return { error };
}
}
@syed-ahmad
Copy link

Nice one!

@karlhorky
Copy link
Author

karlhorky commented May 6, 2021

Thanks, glad it's helpful!

@syed-ahmad
Copy link

It looks good to me just going to play with it in development.

@syed-ahmad
Copy link

How would you use that with fetch API?

@AndreiCalazans
Copy link

AndreiCalazans commented May 6, 2021

fetch returns your promise. const { data, error } = await tryCatch(fetch(...));

@syed-ahmad
Copy link

syed-ahmad commented May 6, 2021

Yep i know that, thanks, sorry I didn't explain it well.

Here is my issue, its to do with TS not recognising that data & error exists

image

@karlhorky
Copy link
Author

karlhorky commented May 6, 2021

This doesn't have anything to do with the fetch - it's because the TypeScript type has either {error: Error} or {data: Response}.

You could either change your code:

const result = await tryCatch(promise);

if ('error' in result) return handleError(result.error);

// Here TypeScript is certain that result.data exists:
doSomethingAwesome(result.data);

or change the return type of my function:

export default async function tryCatch<Data>(
  promise: Promise<Data>,
): Promise<{ error: Error, data: undefined } | { error: undefined, data: Data }> {
  try {
    return { data: await promise };
  } catch (error) {
    return { error };
  }
}

Unfortunately, with this approach, the destructuring causes TypeScript to lose track of whether error or data exists - it thinks that both exist always:

Screen Shot 2021-05-06 at 15 48 57

Destructuring + TypeScript still has some downsides (as of May 2021)

@syed-ahmad
Copy link

Thanks @karlhorky, i came to the same conclusion as TS is not happy anyway when you try to destructure that.

Also, thanks for suggesting if ('error' in result) return handleError(result.error); that will do the job but i don't like this construct tbh as you loose the type inference at that point.

Again, really appreciate you taking time out to look into it.

@karlhorky
Copy link
Author

i don't like this construct tbh as you loose the type inference at that point

What do you mean that you lose type inference? I don't see this behavior in the 'error' in result alternative...

@syed-ahmad
Copy link

So, when you do ('property' in object), you are just feeling lucky that it may or may not exist.

@syed-ahmad
Copy link

syed-ahmad commented May 6, 2021

Whereas using TS, the whole point is to leverage type inference and avoid compile time errors.

So, if the underlying contract changes to Promise<{ errorX: Error } | { data: T }>, TS won't show any warning/error for that line 'error' in result

@karlhorky
Copy link
Author

Using "prop" in obj is pretty common practice, check out the TypeScript Handbook section on Narrowing: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-in-operator-narrowing

@syed-ahmad
Copy link

Agreed, i got that wrong, should've played that on machine first before speculating 👍

I think I would still want the destructing to work for me so I can avoid those if blocks before I use that.

@syed-ahmad
Copy link

Using "prop" in obj is pretty common practice, check out the TypeScript Handbook section on Narrowing: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#the-in-operator-narrowing

Sure, will do.

@syed-ahmad
Copy link

Here you go, destructuring works like a charm now 🎉
Try in TS Playground

export default async function tryCatch<T>(
  promise: Promise<T>
): Promise<{ error?: Error; data?: T }> {
  try {
    return { data: await promise };
  } catch (error) {
    return { error };
  }
}

(async function a() {
  const {error, data} = await tryCatch(fetch(""));

 console.log(error, data);
})();

@karlhorky
Copy link
Author

karlhorky commented May 6, 2021

Nice, good job! 🎉

The only reason that I tend to avoid this pattern is because then you cannot properly narrow the type of data.

As soon as you destructure, TypeScript "forgets" the association between error and data, meaning that you'll need to check whether data exists every time (see the | undefined part?):

Screen Shot 2021-05-06 at 18 20 05

@syed-ahmad
Copy link

syed-ahmad commented May 6, 2021

Drat! you are spot on.

I'm just debating with myself if I can live with the following? What do you reckon @karlhorky?

const fn = async () => {
  const promise = fetch(`https://cdn2.thecatapi.com/images/lm.jpg`);

  const { error, data } = await tryCatch(promise);

  if (error) return handleError(error);

  const stuff = await data?.json();

  doSomethingWith(stuff);
};

@karlhorky
Copy link
Author

Yeah, I started with this pattern too, using the optional chaining. I think it's not so bad for one usage!

But if you've got a long file where you're using data in a lot of places, then it may be worth it to consider avoiding the destructuring in the first step. That's where I ended up with most of my code.

@syed-ahmad
Copy link

syed-ahmad commented May 6, 2021

I'm going to go with optional chaining as I tend to keep my functions small, let's see how it goes.

Great chatting to you.

@karlhorky
Copy link
Author

Thanks, I enjoyed the journey too! 🙌

@syed-ahmad
Copy link

Hi @karlhorky, is it okay if I create an npm package for that with tests?

@karlhorky
Copy link
Author

karlhorky commented Jun 29, 2021

TS 4.4 may no longer "forget" the type information after destructuring: https://twitter.com/sebastienlorber/status/1409543348461965314?s=19

@karlhorky
Copy link
Author

Ok, this is true for a single variable, but it doesn't apply for multiple variables being destructured: https://stackoverflow.com/a/59786171/1268612

@karlhorky
Copy link
Author

It looks like this is actually a separate feature request called "Correlated Unions" by jcalz here: microsoft/TypeScript#30581

@karlhorky
Copy link
Author

Seems like this may be in TypeScript 4.6, since this PR got merged:

microsoft/TypeScript#46266

@syed-ahmad
Copy link

Thanks @karlhorky

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment