JavaScript is a single-threaded language, which means it runs one task at a time. These threads are event-based and only triggered by the occurrence of events.

Typically, a program needs to execute outside of the normal flow to give a better application performance without breaking other parts of the program. However, the JavaScript language was not designed to allow the simultaneous execution of multiple statements. Statements run one after the other. How is JavaScript able to perform non-blocking tasks without interfering with other functions?

In this article, you will learn the following:

  • The definition of the term concurrency.
  • How JavaScript executes non-blocking functions.
  • Event loops, callbacks, async/await, and promises.

To understand the JavaScript concurrency model, let’s proceed to understand some key terms and how the JavaScript concurrency model works.

The call stack, heap, and queue

Although JavaScript is a single-threaded language, the browser is strong enough to carry out its complex operations. Call stacks, heaps, and queues are all a part of the browser, which allow it to perform these operations.

Heap: A place or memory where objects and variables from our code are stored when they are defined. It is the memory allocation to variables and objects inside the browser.

Callback queue: This is where codes are pushed and stored before they are executed. The queue is processed based on the first-in, first-out(FIFO) principle.

Call stack: The call stack holds function calls. During the execution of the program, each function is pushed to the top of the stack and gets popped out after it completes. It is a simple data structure that records the function called during the program's execution.

Each function creates a stack frame containing expressions and statements, after which it gets added to the stack. When it returns a value, it gets popped out and moved to the subsequent function.

Stack frames are created for each function nested inside one another. Items in the stack are usually in first-in, last-out (FILO) order.

What is concurrency

Concurrency allows for multiple computations to happen at the same time. It is used to describe the idea of performing multiple tasks at once. It should not be confused with parallelism.

Concurrency refers to performing different tasks simultaneously with differing goals, while parallelism pertains to various parts of the program executing one task simultaneously.

  • Parallelism is a type of computation used to make programs run faster. In this programming approach, tasks or operations are executed in parallel. Programs are split into tasks and performed simultaneously to achieve the same goal.
  • Multi-threading is a programming technique in which two or more instructions are executed independently while sharing the same resource. JavaScript is, by design, not a multi-threaded language, but with the use of modern-day web workers, multi-threading can be achieved.

How does concurrency work?

JavaScript's concurrency model is based on an event loop, which executes the code and processes events. JavaScript handles concurrency by default with an asynchronous programming model using event loops, callbacks, promises, or async/await. Some languages, such as Rust and Go, offer simplicity and performance when executing concurrent tasks. However, JavaScript was not designed for concurrency, but with the JavaScript event loop, it can perform server-side and client-side concurrency.

Event loop

The JavaScript event loop is the model adopted by JavaScript to execute functions asynchronously. It is responsible for executing queued tasks, and it is the secret behind JavaScript’s ability to perform multithreading tasks.

JavaScript starts a script execution from the top and continues until the last line of execution. Typically, functions that are to be executed are stored in the callback queue.

The event loop constantly monitors both the callback queue and the callback stack. It checks whether the call stack is empty and pushes the next item in the callback queue to the call stack. The event loop does nothing if the call stack isn't empty. It waits until the call stack is empty and pushes the next function from the callback queue to the stack.

The following picture illustrates the JavaScript runtime, Web API, call stack, and event loop:

Visual illustration of the how event loop works

The event loop constantly pulls functions out of the callback queue and pushes them to the call stack whenever the call stack becomes empty. After the task in the call stack completes, the event loop takes the next item in the callback queue and sends it to the call stack to start executing. This is the basic principle of how the event loop works.

Callbacks

Node.js makes heavy use of callbacks to perform quick I/O operations. Callbacks are functions, passed as arguments to another function. In JavaScript, callbacks are used to reduce unexpected freezing of applications.

A callback function is executed after the completion of the outer function, which takes it as an argument. It is used to run a function immediately after the completion of another function. The setTimeout() method is an example of a function that takes another function as an argument. Here is an example:

  const greeting = function() {
    console.log("Hello there")
}

  setTimeout(greeting, 3000)

A callback function (greeting) is passed as an argument to the setTimeout() method, and it executes immediately after the setTimeout() completes its execution of 3 seconds.

Callback functions allow JavaScript to execute codes asynchronously. While the setTimeout() function executes, other statements can execute simultaneously. Using this method, JavaScript can handle concurrency and execute multiple statements simultaneously.

Promises

Promises are similar to callbacks in the sense that they can both be used to handle asynchronous tasks in JavaScript. Prior to the adoption of promises, callbacks were used to handle tasks asynchronously. However, handling multiple nested callbacks became a problem and resulted in callback hell, which caused unnecessary complexity to the program. Promises became the ideal way to handle multiple nested callbacks.

A promise is an object that produces a value after an asynchronous operation. It is a good way to determine whether the asynchronous operation is successful. An example of the use of a promise is written below:

  let promise = new Promise(function (resolve, reject) {
    resolve("done")
    reject("error")
}

  promise.then(function(value) {
    console.log(value)
})

A promise uses the .then and .catch methods to consume resolved and rejected promises.

Since promises run asynchronously, functions whose operation depends on the promise's outcome should be placed in the .then method, as shown in the code above. It would only run when the promise is gotten.

Async/await

Using and chaining promises together gets bulky and confusing, async/await was introduced to solving this problem. It is used to efficiently run and identify asynchronous codes.

The async keyword is placed before a function to identify an asynchronous function and make sure the function always returns a promise.

The await keyword delays the function until a promise returns. A function will not complete its execution until a promise is received.

The async and await keywords work together to perform and run JavaScript code asynchronously. The await keyword cannot be used outside an async function. Here is an example of an async/await code:

  async function() {
    await setTimeout(() => {
      console.log("Welcome Back!")
    }, 2000)
}

  console.log("Hello")

Asynchronous functions take time to process and can run simultaneously in the background without blocking the execution of other processes.

Asynchronous functions in JavaScript are identified by the async keyword. This tells JavaScript that the function will take time before it finishes its execution and can go on with other tasks while it executes(i.e., run as a background process).

This allows JavaScript to run other operations outside the async function without waiting to get a response from the function. The await keyword identifies the part of the function that takes time to complete its execution. Here is the result of the code shown above:

  Hello
  Welcome Back!

As you can see, “Hello” displays before the “Welcome Back!” message. This is because the async function runs in the background and takes about two seconds to display “eat”. While executing the function in the background, JavaScript continues executing other statements without waiting for a result from the async function.

This is how JavaScript can perform tasks and execute statements concurrently in an overlapping manner.

Callbacks, promises, and async/await are JavaScript features that make it possible to run tasks concurrently with the help of the event loop and the browser's web APIs.

Conclusion

There are a lot of problems with sequential code execution when processing data. It is difficult to know how long it will take to process the data. A sequential code will block all other codes and prevent the application from further execution.

Concurrent code eliminates the blocking problem of sequential coding and can handle multiple user requests or events simultaneously without any problems.

JavaScript is, by default, a single-threaded programming language and runs its code sequentially. However, JavaScript is not the best choice when building CPU-intensive applications because JavaScript is still a single-threaded language and can run a single process on a single core.

Other languages capable of handling heavy concurrent tasks should be considered when developing CPU-intensive applications.

Get the Honeybadger newsletter

Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo
    Salem Olorundare

    Salem is a software engineer who specializes in JavaScript. He enjoys breaking down advanced technical topics in his writing so they can be understood by people of all skill levels.

    More articles by Salem Olorundare
    An advertisement for Honeybadger that reads 'Turn your logs into events.'

    "Splunk-like querying without having to sell my kidneys? nice"

    That’s a direct quote from someone who just saw Honeybadger Insights. It’s a bit like Papertrail or DataDog—but with just the good parts and a reasonable price tag.

    Best of all, Insights logging is available on our free tier as part of a comprehensive monitoring suite including error tracking, uptime monitoring, status pages, and more.

    Start logging for FREE
    Simple 5-minute setup — No credit card required