Originally published at coreycleary.me. This is a cross-post from my content blog. I publish new content every week or two, and you can sign up to my newsletter if you'd like to receive my articles directly to your inbox! I also regularly send cheatsheets and other freebies!
"You should really use Promises or async/await here to make this more readable"
How many times have you posted some code snippet when trying to get an answer to your question, and someone ends up pestering you about this? Now, on top of whatever problem you already have with your code, you have another thing you need to learn and "fix"...
Or what about dealing with refactoring an existing, callback-based codebase at work? How do you convert them to native JavaScript Promises? It would be so great to be able to develop using modern JavaScript and start making use of the async/await
functionality...
If you knew how to avoid callbacks, you could post your code online when asking for help without people asking you to rewrite it and not actually answering your question.
And if you were refactoring an existing codebase, that code would be more readable, you could avoid the "callback hell" people still seem to talk about even in 2019 when Promises have had support in many browsers and Node for years now, and async/await
is supported by many versions as well...
The fix
Let's go over how to convert those old-school callbacks to Promises and to async/await
versions.
Here's the link to the code demonstrating the callback -> Promise and callback -> async/await
versions.
Callback version
const callbackFn = (firstName, callback) => {
setTimeout(() => {
if (!firstName) return callback(new Error('no first name passed in!'))
const fullName = `${firstName} Doe`
return callback(fullName)
}, 2000)
}
callbackFn('John', console.log)
callbackFn(null, console.log)
You'll notice here that we're using the setTimeout()
function in order to make our function asynchronous. In addition to setTimeout()
, other asynchronous operations you're likely to see in the real-world are: AJAX and HTTP calls, database calls, filesystem calls (in the case of Node, if no synchronous version exists), etc.
In this function, we "reject" it if the first name argument is null. When we do pass in the firstName
argument, the callback function (almost always the last argument in a callback-based function's argument list) gets called and returns our value after the 2 seconds set in setTimeout()
.
If we don't pass in a callback, we get a TypeError: callback is not a function
error.
Promise version
And here's the Promise-based version of that function:
const promiseFn = firstName => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (!firstName) reject(new Error('no first name passed in!'))
const fullName = `${firstName} Doe`
resolve(fullName)
}, 2000)
})
}
promiseFn('Jane').then(console.log)
promiseFn().catch(console.log)
Converting to a Promise-based function is actually pretty simple. Look at the below diagram for a visual explanation:
First, we remove the callback argument. Then we add the code to return a new Promise
from our Promise-based function. The error callback becomes a reject
, while the "happy path" callback becomes a resolve
.
When we call the promiseFn
, the result from the happy path will show up in the .then()
, while the error scenario will show up in the .catch()
.
The great thing about having our function in Promise form is that we don't actually need to "make it an async/await version" if we don't want to. When we call/execute the function, we can simply use the async/await
keyword, like so:
const result = (async () => {
try {
console.log(await promiseFn('Jim'))
} catch (e) {
console.log(e)
}
try {
console.log(await promiseFn())
} catch (e) {
console.log(e)
}
})()
Side note: here I wrapped the function call in an IIFE - that's what that (async () => {....})()
is if you've never seen it. This is simply because we need to wrap the await
call in a function that uses the async
keyword, and we also want to "immediately invoke" the function (IIFE = "Immediately Invoked Function Execution") in order to call it.
Here, there are no callbacks, no .then()
's or .catch()
's, we just use a try/catch
block and call the promiseFn()
. Promise rejections will be caught by the catch
block.
Note: async/await
is available in most semi-recent releases of the major browsers, with the exception of Internet Explorer. Node has had support for the feature since version 7.6.0
async/await version
But what if we wanted to convert a callback function directly to an async/await
version of that function? Without using Promises directly?
async/await
is syntactic sugar around Promises, so it uses them under the hood. Here's how you can convert it:
const timeout = ms => {
return new Promise(resolve => setTimeout(resolve, ms))
}
const asyncAwaitFn = async firstName => {
await timeout(2000) // using timeout like this makes it easier to demonstrate callback -> async/await conversion
if (!firstName) throw new Error('no first name passed in!')
const fullName = `${firstName} Doe`
return fullName
}
const res = (async () => {
try {
console.log(await asyncAwaitFn('Jack'))
} catch (e) {
console.log(e)
}
try {
console.log(await asyncAwaitFn())
} catch (e) {
console.log(e)
}
})()
Use the below diagram to understand how to go from callback to async
:
Similar to converting to the Promise-based version, we get rid of the callback passed in to the original function, as well as that argument call within the body of the function. Next, we add the async
keyword to the beginning of the function declaration. And finally, when we hit the error scenario, we throw an Error
, which results in a rejected Promise (caught in the catch
block when we call the function), and simply return the fullName
in the happy path scenario.
Note that async
functions all return Promises, so when you use return
you are just resolving the Promise.
Wrapping up
Next time you need to convert a callback-based function to a Promise-based one or an async/await
-based versions, use the visual diagrams from this post to quickly and easily do so. And if you need some code to play around with to help the concepts settle some more, here's the link again to the code demonstrating the callback -> Promise, and callback -> async/await
versions.
Callback hell is now gone!
I have plenty more content planned for the future, so if you found this helpful and want to receive it directly to your inbox without having to remember to check back here, here's the link again for the newsletter!
Top comments (10)
I remember this picture
I don't know if there a promise hell example. Thanks for this great article.
Promise hell can't exist, that's why it's so nice!
See? It's really clean 😃
Haha yeah this one's classic - good point too, you can hit "promise hell" too if you're not careful. Not as "pyramid" like as callback hell, but having ton's and tons of
then
's can get sort of annoyingThink also if it's also with If-Else Statements :)
I can appreciate trying to dispense some knowledge, but why not use one of those "real-world" cases in the example instead of the highly abstract
setTimeout()
?I've been trying to grasp
promise
s andasync/await
for "a while" now, but examples and articles I find like this, as you've said, use the wholly unrealisticsetTimeout()
.I completed a personal project a few months ago in NodeJS that was pretty standard AJAX activity . . .
It took finishing it to realize that I built a several indention deep callback hell. Great. Well at least now I have a base to find examples for promises.
Not. Every example on the first page of my various google searches all used
setTimeout()
.I'd love to see an example of callback > promise > async/await based on, as you've said, realistic file, database or CORS calls.
Real example and also I would simplify the error handling a ton and lose the try catch.
You can pass anything that returns a promise to that module like so.
So here we are using the WP backbone api to return a collection of whatever and checking if more exist each time, when its all done we are combining them all together and loading the result into the data variable so we can stop our loading event on the table and show all the results. The js api will only return 100 items in a collection at a time and in this case we needed to grab 200-300 of whatever and load the table with all the data so the searching and sorting is instant so we sacrifice the inital time to grab all the data from better UI once we do.
PS: This is the basic layout of a Vue component, I just left off the methods wrapper around the loadItems function so you could see it easier.
With async await and your example you described you would basically have a method or function returning a promise for checking on if the file exists and reading the data from it, probably using axios or fetch. Then another method returning a promise to create and write to a file. Each method would return a response and you would await on the result before doing the next part.
Hope this gave you a real world example to see how this stuff can be used.
Yeah I've got another post in my backlog that will be something more along the lines of, finding some open source code snippet written using callbacks and refactoring the whole thing to show the before and after.
This post is really meant more to show the process, and I didn't want to overwhelm with too much information. However I understand some people learn better if they see something taken from an existing application and rewritten.
Thanks for the comment
We can also use
util.promisify()
to convert callback to promiseyep. works like a charm! :)
devdocs.io/node/util#util_util_pro...
Use bluebirds .from callback function to easily to await on a function that takes a callback).
This only works if your callbacks are called in the standard way though, ie (err, result).
const Promise = require('bluebird');
await Promise.fromCallback(callback => functionThatTakesACallback(arg1, arg2, callback));