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();
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());
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);
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);
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);
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.
Top comments (18)
Hi Sunny, very interesting article. Thanks!
I especially loved this one-liner:
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):This is using two
await
keyword, but with parenthesis to control the order of theawait
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!
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()
).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.
From the documentation, it is stated that
So I guess this is the case when the fetch method can throw an error.
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
What do you think about this approach
medium.com/@dimpapadim3/asynchrono...
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).
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.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 withasync/await
.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.
Agree, it's generally a good idea to be explicit and prevent any edge cases.
So, you're basically using the
then
andcatch
methods of a promise and then adding anawait
in front of it? Why not just handle the data with anotherthen
?I mean, once you start writing code like this:
You might as well put the
handleData
expression into its ownthen
.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 usecallbacks 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.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
andreduce
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: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?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
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.
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.
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.