Any general purpose programming languages must provide a way of writing asynchronous code to handle heavy tasks — I/O operations, calling network requests, writing to files, etc — and JavaScript is obviously one of them.
Asynchronous programming in JavaScript is quite confused in term of availability between language standard library, runtime environment, third-party libraries, transpilers and polyfills. Let’s look at the evolution of how we handle asynchronous tasks in JavaScript since its inception.
Concurrency vs Parallelism — this is what we ultimately wants when writing code to execute heavy tasks, concurrency means executing tasks virtually at the same time (start, run, and complete in overlapping time periods, in no specific order), parallelism means executing tasks literally physically at the same time (requires hardware with multiple processing units).
Parallelism can be achieved at different levels like bit, instruction, thread, task, data, memory, loop, pipeline. Parallelism and concurrency is closely related to each other even though they are distinct — it is possible to have parallelism without concurrency, concurrency without parallelism, or both at the same time.
Developer can achieve different levels of parallelism and concurrency depending on the complicated combination of hardware, operating system and programming language — they are beyond the scope of this article. Developer often cares only about achieving concurrency of tasks by following concurrency model of programming language and let true parallelism optimized by lower levels of abstraction.
Browser’s JavaScript runtime is conceived to be single-threaded, in modern browsers each tab or web worker is a thread. Node.js runtime is also single-threaded by default, you can create thread if you run Node.js v10.5.0 or higher.
JavaScript has a concurrency model based single thread event loop — executing the code, collecting and processing events, and executing queued sub tasks.
Synchronous vs Asynchronous — this is programming model, synchronous means tasks are executed in sequence, and asynchronous means tasks started in sequence but don’t need to wait for completion before moving to next tasks.
Blocking vs Non-blocking — refers to whether a task blocking even loop or not, blocking tasks execute synchronously and non-blocking tasks execute asynchronously.
So ultimately in JavaScript we want to use as many asynchronously non-blocking tasks as possible to achieve better concurrency — improved application performance and enhanced responsiveness. This post focuses on different styles of writing asynchronous programming in JavaScript using callbacks, promises, and async/await.
Callback in JavaScript is a function that is passes as argument to other function that is expected to execute (call back) the argument at a given time; synchronous callbacks are invoked before a function returns, asynchronous callbacks may be invoked after a function returns.
Errors in callbacks can be handled by providing the first parameter in any callback function as the error object. If there is no error, the object is null.
Every callback adds a level of nesting, and when you have lots of callbacks, the code starts to be really doom — infamously known as callback hell.
function verifyGoogleToken(idToken, callback) {
/* ... */
}
function createUserIfNeeded(data, callback) {
/* ... */
}
function signAccessToken(user, callback) {
/* ... */
}
function loginWithGoogle(idToken, callback) {
verifyGoogleToken(idToken, (err, googleProfile) => {
if (err) {
return callback(err, null)
}
createUserIfNeeded(googleProfile, (err, createdUser) => {
if (err) {
return callback(err, null)
}
signAccessToken(createdUser, (err, result) => {
if (err) {
return callback(err, null)
}
callback(null, result)
})
})
})
}
Remember that callbacks are a fundamental part of JavaScript and you should learn how to read and write them before moving on to more advanced language features, since they all depend on an understanding of callbacks.
A promise is an object that may produce a single value some time in the future, either a resolved value, or a reason that it’s not resolved, may be in one of 3 possible states — fulfilled, rejected, or pending.
function verifyGoogleToken(idToken) {
return new Promise((resolve, reject) => {
/* ... */
})
}
function createUserIfNeeded(data) {
return new Promise((resolve, reject) => {
/* ... */
})
}
function signAccessToken(user) {
return new Promise((resolve, reject) => {
/* ... */
})
}
function loginWithGoogle(idToken) {
return new Promise((resolve, reject) => {
verifyGoogleToken(idToken)
.then(createUserIfNeeded)
.then(signAccessToken)
.then((result) => {
/* ... */
})
.catch((err) => {
/* ... */
})
.finally(() => {
/* ... */
})
})
}
Async functions enable us to write promise based code as if it were synchronous, but without blocking the execution thread. It operates asynchronously via the event-loop.
An async function can contain an await expression that pauses the execution of the async function and waits for the passed Promise’s resolution, and then resumes the async function’s execution and evaluates as the resolved value.
The purpose of async/await functions is to simplify the behavior of using promises synchronously and to perform some behavior on a group of Promises. Just as Promises are similar to structured callbacks, async/await is similar to combining generators and promises.
async function verifyGoogleToken(idToken) {
/* ... */
}
async function createUserIfNeeded(data) {
/* ... */
}
async function signAccessToken(user) {
/* ... */
}
async function loginWithGoogle(idToken) {
try {
const googleProfile = await verifyGoogleToken(idToken)
const createdUser = await createUserIfNeeded(googleProfile)
const result = await signAccessToken(createdUser)
// do something else with result or return
} catch (e) {
// handle error or rethrow
} finally {
// clean up if needed
}
}
JavaScript is no longer confined to the browser. It runs everywhere and on anything, from servers to IoT devices. Many of these programs are heavy weight and might tremendously benefit from asynchronous programming.
Asynchronous programming model helps us to achieve concurrency. Asynchronous programming model in a multi-threaded environment is a way to achieve parallelism.
What is the best ways to write asynchronous code in JavaScript? Just callbacks, or promises, or promises with generators. It’s your call! Callbacks is the fastest solution possible at this time, promises with generators give you opportunity to write asynchronous code in synchronous fashion.