Callbacks, Promises, and Async/Await
The JavaScript engine is single threaded and utilises an event loop. Simply put, this means any statements you run will be executed one after the other in a single process. To avoid blocking calls, there are a number of techniques that JavaScript employs to avoid waiting while something is computed. These are asynchronous functions.
You can read more about the event loop here as the topic is too deep to cover in this post.
JavaScript provides three methods of handling asynchronous code: callbacks, which allow you to provide functions to call once the asynchronous method has finished running; promises, which allow you to chain methods together; and async/await keywords, which are just some syntactic sugar over promises.
Callbacks
The original method of handling asynchronicity. Callbacks allow you to provide a function to be executed after the asynchronous code has finished. In the example below, functionWithACallback
takes a function as an argument and will call that function when it is finished.
This method, passing functions back and forth, can become very confusing if you need to chain a number of these calls together. The callback will need to be passed down the execution chain to be called at the end of the final process.
const functionWithACallback = (callback) => {
//do some work then call the callback once done
console.log('You called this function!');
setTimeout(() => {
callback('I am done');
}, 1000)
};
const myFunction = () => {
// the function we want to call when the work is done
const myCallback = console.log
// this will call myCallback once it is finished
functionWithACallback(myCallback);
};
myFunction();
// You called this function
// I am done
Promises
One of the main problems with callbacks is, when chaining a number of function calls together it can become increasingly difficult to follow the flow of execution. Promises aim to solve this issue by allowing you to chain together promises using the .then()
syntax. The example below works the same way as the callback example, but is much easier to follow: wait till getPromise()
has completed and then call the function containing console.log()
Error handling with promises is also less complex. Rather than calling the callback with an error object, promises provide a .catch()
wrapper to help manage error states. Below, the catch
block will be executed if an error occurs in any of the promises above it.
const getPromise = () => Promise.resolve('My return value');
const myFunction = () => {
getPromise()
.then(val => {
console.log(val); // prints 'My return value'
}) // If there is an error in any of the above promises, catch
// it here
.catch(error => {
console.error(error.message);
});
}
Async/Await
In the more recent versions of JavaScript, the async
and await
keywords were added. This provides a cleaner method of writing promises and gives the user more control over execution order. The below example is identical to the promises example in functionality, but is written using the async
and await
keywords.
Error handling for async
function calls is provided using a try/catch
block.
const getPromise = () => Promise.resolve('My return value');
// the function is marked with the async keyword
const myFunction = async () => {
// tell the interpreter we want to wait on the response
try {
const val = await getPromise();
// execute when the promise above is resolved
console.log(val); // prints 'My return value'
} catch (error) {
console.error(error.message);
}
}
Top comments (8)
In the last example, after the await keyword: does the program continue executing until the variable val is required (and waited upon) or does it "block" there?
That is, if there is something between the await and the console.log(val), will it be executed immediately or only once the promise is resolved?
Hi. Yes it's a blocking call. If you want to start the asynchronous function, do some other work, then wait for the return instead you can go with something like this:
It blocks until the promise resolves
await isn't blocking, it creates a continuation from all the code within the async function that appears after the await statement. That continuation runs when the thing being awaited resolves.
An async function implicitly returns a promise when it hits the first await statement and control is returned to the calling code which will continue to run.
If there was a return value from myFunction, that is also wrapped in a promise and you'd need to use an await or a .then() to use it.
How about observer pattern e.g. rxjs?
Not native but can't really be overlooked as it's the most readable and powerful of the lot IMO and has the advantage over all the others that they're cancellable.
I think for most people native async management is enough. rxjs is a entity unto itself and would require it's own write up, and the observer pattern in general comes with it's own set of pros and cons.
Don't be discouraged. Someday our native Observables dream will come true.
Great quick explainer! Nice to have something to point to for folks.