How to rewrite a callback function in Promise form and async/await form in JavaScript
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.
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, sign up below:
Subscribe for more JS, Node and testing content!
No spam ever. Unsubscribe any time.