DEV Community

loading...
Cover image for Simplify JavaScript Promises

Simplify JavaScript Promises

Sunny Singh
Creating content and code
・2 min read

I love promises. Not from people, but from JavaScript.
Tweet Quote

I love promises. Not from people, but from JavaScript. Promises make your code concise and simple, resulting in easier to understand codebases.

You may also be familiar with the async/await syntax, but unfortunately it causes some headaches. I'll walk through some techniques that solve common scenarios.

Combining async/await with Promise.then

The first problem that I encountered is the verbosity of using fetch:

const response = await fetch('/api');
const data = response.json();
Enter fullscreen mode Exit fullscreen mode

If you're relying solely on just using await, then you will end up using multiple variables and lines for very simple use cases.

Instead, we can take advantage of the "traditional" Promise.then syntax:

const data = await fetch('/api').then(res => res.json());
Enter fullscreen mode Exit fullscreen mode

A one-liner that is still readable and functions the same way.

Combining async/await with Promise.catch

The second problem that I encountered is the scope created with try { } blocks:

try {
  const data = await fetchData();
} catch (error) {
  console.error(error);
}

// Oh no, `data` is undefined 😱
console.log(data);
Enter fullscreen mode Exit fullscreen mode

Hmm... we can't read data outside of the try { } block. If you're new to the const variable I suggest you read my demystifying const variables article, but essentially this variable is scoped to only be used inside of its curly braces { }.

One thing we could do is to lift the variable up:

let data;

try {
  data = await fetchData();
} catch (error) {
  console.error(error);
}

// Now we can use `data` 😎
console.log(data);
Enter fullscreen mode Exit fullscreen mode

But... we are no longer within the safe bounds of using const variables. Anywhere later on in the code, data could get reassigned and we'd be spending hours debugging. Is there a way to get the same result while still using const?

Why yes, there is:

const data = await fetchData()
  .catch(error => {
    console.error(error);
    return null;
  });

// We can still use `data` 👍
console.log(data);
Enter fullscreen mode Exit fullscreen mode

We're again taking advantage of two syntaxes for a Promise: async/await and Promise.catch. If fetchData resolves successfully, then that value is set to the data variable as usual. Otherwise, the data variable gets set to null which is what gets returned inside of .catch().

Refactor wisely

When a new language feature comes out, developers rush to make their codebase follow that new syntax. As you saw in this article, this is not always wise. Combining older syntax with the new can be a powerful way to keep your codebase simple and easy to understand for anyone new to it.

Discussion (18)

Collapse
aminnairi profile image
Amin

Hi Sunny, very interesting article. Thanks!

I especially loved this one-liner:

const data = await fetch('/api').then(res => res.json());

I think you can be even more concise and completely ditch the use of the promise.then method by using this example (inspired by yours, slightly modified so it can be easily tested):

"use strict";

async function main() {
    const api = "https://jsonplaceholder.typicode.com/users";
    const users = await (await fetch(api)).json();

    console.log(users);
}

main().catch(console.error);

This is using two await keyword, but with parenthesis to control the order of the await flow.

I also added a simple catch to handle errors (even if I'm not doing interesting stuff with it, yet).

What do you think of that? Is it better or worse? Let me know!

Collapse
sunnysingh profile image
Sunny Singh Author

Definitely more concise!

I think there is something to be said about how a .then reads a bit better, but this can be argued both ways. Either example takes advantage of the language to do one thing: fetch data.

In most scenarios, you would have a helper function anyway, which is essentially what you wrote (I would just rename it to something like fetchUsers()).

Collapse
bushblade profile image
Will Adams

Just wanted to add in that fetch doesn't throw errors or reject so .catch won't fire there. You need to throw your own errors with fetch.

Collapse
aminnairi profile image
Amin

From the documentation, it is stated that

A fetch() promise only rejects when a network error is encountered

So I guess this is the case when the fetch method can throw an error.

Thread Thread
bushblade profile image
Will Adams • Edited

Yeah you'll want to throw your own errors if you're using fetch and want to catch any errors. Something to be aware of otherwise most of your 'errors' will end up in your .then

Collapse
dimitris_papadimitriou profile image
dimitrispapadimitriou

What do you think about this approach
medium.com/@dimpapadim3/asynchrono...

Collapse
aminnairi profile image
Amin

I like it! Even if I personally think that this API could be sexier but I'm a big fan of FP and its concept (especially in Elm, Haskell or Rust).

Collapse
kayis profile image
K • Edited

Nice article, there needs to be more about promises because their error handling is a bit finnicky.

Always add a .catch(), because promises will swallow your errors without any second thought.

Also, remember to re-throw your error inside the .catch() handler if you only handle a specific type of error, because the other error types will also end up there and be swallowed as well.

Collapse
sunnysingh profile image
Sunny Singh Author

Are you sure that you always need to add a .catch()? As long as an error is thrown, and you don't swallow the error yourself inside of your own .catch(), then it should keep bubbling up until it reaches an error handler. I could be wrong but this is the behavior I'm used to seeing with async/await.

Collapse
kayis profile image
K

I think you're right.

But if you deep down it can always be the case that there is a catch somewhere above that eats errors.

Thread Thread
sunnysingh profile image
Sunny Singh Author

Agree, it's generally a good idea to be explicit and prevent any edge cases.

Collapse
savagepixie profile image
SavagePixie • Edited

So, you're basically using the then and catch methods of a promise and then adding an await in front of it? Why not just handle the data with another then?

I mean, once you start writing code like this:

const data = await fetch('api/url')
    .then(res => res.json())
    .catch(handleError)
handleData(data)

You might as well put the handleData expression into its own then.

fetch('api/url')
    .then(res => res.json())
    .catch(handleError)
    .then(handleData)
Collapse
sunnysingh profile image
Sunny Singh Author • Edited

It depends on the flow that you want in your code.

The benefit of async/await is not having to deal with callbacks and maintain what looks like a synchronous flow.

However, only using async/await has drawbacks, so that's where you might want to use
callbacks in .then() or .catch() to only handle some things like data transformation and error catching.

So you could handle your data with another .then(), and that's totally valid. Just depends on code style preference.

Collapse
savagepixie profile image
SavagePixie

There are a couple of things that confuse me.

First, I guess I don't see what's the problem with callbacks. I mean, I know what people used to call callback hell and all that, but callbacks don't always generate it. In fact, functions as first class citizens is an amazing feature of JavaScript (map, filter and reduce being good examples of why). So I don't quite understand why not having to deal with callbacks would be a benefit.

Second is that you're already dealing with callbacks. You've got callbacks when you pass data through a then and when you handle errors. So I don't quite see what you're avoiding by using this method.

That's why I am a bit confused as to what you're supposed to be improving on. Mind you, I'm not saying that you're not getting anything out of it. I'm saying that I don't see what you're getting out of it that wouldn't be improved by ditching async/await altogether.

Bonus track: A lot of people do say that async/await makes your asynchronous code look synchronous. I find it an odd statement, though, as promise syntax looks a lot more like these synchronous snippets of code:

const transformNumbers = array => array
    .filter(isEven)
    .map(double)
    .map(addOne)
    .reduce(getMinMax, {
        min: Infinity,
        max: -Infinity
    })
const checkAdjacent = number => number 
    .toString()
    .split('')
    .some((x, i, a) => x == a[i - 1])

And more synchronous code would look like that if the TC39 guys finally implemented the pipeline operator.

What async/await does seem to do is to make asynchronous code look more imperative than promises. So maybe that's a thing you're getting by using your method?

Collapse
blazephoenix profile image
Tanmay Naik

This is very well explained. I honestly found Promises a bit confusing initially in terms of understanding what's happening in the code so I stuck to async/await to avoid complicating it for myself. This helped clear it up. Well probably start using them more now

Collapse
sunnysingh profile image
Sunny Singh Author

Super happy to hear that Tanmay! The async/await syntax actually helped me understand how a Promise works better, but it definitely took a long time.

Collapse
kmwill23 profile image
Kevin

This is actually the first useful example of promises I have seen. I am normally not a fan of them.

I am an old callback fan. Didn't begin to move past them until async/await.

Collapse
sunnysingh profile image
Sunny Singh Author

Thanks Kevin! I'm in the same boat, haven't really seen a huge value of Promises until async/await, but after still experiencing some headaches with them I realized there's some valid use cases in using both syntaxes.