DEV Community

Cover image for JavaScript: Single-threaded and Synchronous/ Asynchronous Nature
codingKrills
codingKrills

Posted on

JavaScript: Single-threaded and Synchronous/ Asynchronous Nature

JavaScript: Single-threaded and Synchronous/ Asynchronous Nature

JavaScript is a synchronous and single-threaded language by default, meaning it can only execute one task at a time in a sequential manner. However, it can handle asynchronous operations due to mechanisms like:

  1. Event Loop: JavaScript uses an event loop to manage and execute asynchronous operations, such as I/O operations, timers, or network requests. While JS remains single-threaded, it can delegate these tasks to the environment (e.g., the browser or Node.js) to handle in the background.

  2. Callbacks, Promises, and Async/Await: These are constructs that enable asynchronous programming in JavaScript, allowing tasks to be performed "in the background" while still maintaining its single-threaded nature.

Though JavaScript is single-threaded, you can perform non-blocking operations effectively with these mechanisms.


Synchronous Nature of JavaScript

JavaScript runs synchronously, meaning it executes one line of code at a time, in order. If one operation takes time (like a long calculation), it blocks the execution of subsequent code.

Example of Synchronous Code:



console.log('Start');

for (let i = 0; i < 1000000000; i++) {} // A time-consuming loop

console.log('End');


Enter fullscreen mode Exit fullscreen mode

Output:



Start
End


Enter fullscreen mode Exit fullscreen mode

In this example, the time-consuming loop blocks the execution, so End is logged only after the loop finishes.


Handling Asynchronous Tasks

To prevent blocking, JavaScript uses asynchronous mechanisms to deal with time-consuming tasks (like network requests, file I/O) in the background. Hereโ€™s how it works:

  1. The Call Stack: Where JavaScript keeps track of what function is being executed.
  2. Web APIs (or Node APIs in Node.js): Handle tasks that take time (like setTimeout, fetch).
  3. Task Queue: When these tasks are finished, they are pushed into a queue.
  4. Event Loop: The event loop constantly checks if the call stack is empty and then pushes tasks from the queue onto the call stack for execution.

Using Callbacks

Callbacks are one of the first ways JavaScript dealt with asynchronous behavior.

Example with a Callback:



console.log('Start');

setTimeout(() => {
  console.log('Inside setTimeout');
}, 2000); // A 2-second delay

console.log('End');


Enter fullscreen mode Exit fullscreen mode

Output:



Start
End
Inside setTimeout


Enter fullscreen mode Exit fullscreen mode

Promises

Promises provide a more elegant way to handle asynchronous tasks. Instead of passing a callback, a Promise represents the eventual completion (or failure) of an asynchronous operation.

Example with Promises:



console.log('Start');

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise resolved');
  }, 2000);
});

promise.then((result) => {
  console.log(result); // Logs after 2 seconds
});

console.log('End');


Enter fullscreen mode Exit fullscreen mode

Output:



Start
End
Promise resolved


Enter fullscreen mode Exit fullscreen mode

Async/Await

async/await is a more recent feature in JavaScript that provides a cleaner way to handle promises.

Example with Async/Await:



console.log('Start');

async function fetchData() {
  const result = await new Promise((resolve) => {
    setTimeout(() => {
      resolve('Data fetched');
    }, 2000);
  });
  console.log(result);
}

fetchData();

console.log('End');


Enter fullscreen mode Exit fullscreen mode

Output:



Start
End
Data fetched


Enter fullscreen mode Exit fullscreen mode

The Event Loop in Detail

The event loop is what makes JavaScript non-blocking despite being single-threaded. Hereโ€™s a simplified flow:

  1. When you run code, synchronous tasks are pushed onto the call stack and executed immediately.
  2. Asynchronous functions (e.g., setTimeout, fetch) are offloaded to the Web APIs (or Node.js APIs), and JavaScript continues executing other code.
  3. Once these asynchronous operations are complete, the callbacks are pushed to the task queue.
  4. The event loop keeps checking if the call stack is empty. If it is, it pulls the next callback from the task queue and adds it to the call stack for execution.

Example with Event Loop and setTimeout:



console.log('Start');

setTimeout(() => {
  console.log('Timeout 1');
}, 0);

setTimeout(() => {
  console.log('Timeout 2');
}, 0);

console.log('End');


Enter fullscreen mode Exit fullscreen mode

Output:



Start
End
Timeout 1
Timeout 2


Enter fullscreen mode Exit fullscreen mode

Concurrency via Web Workers

Though JavaScript is single-threaded, you can achieve true multi-threading using Web Workers. This allows scripts to run in the background on different threads.

Example with Web Workers:



// In main.js
const worker = new Worker('worker.js');

worker.onmessage = (message) => {
  console.log(message.data);
};

// In worker.js
postMessage('Hello from the worker thread');


Enter fullscreen mode Exit fullscreen mode

This way, JavaScript can run heavy tasks like data processing on a different thread without blocking the main thread.


Summary

  • Single-threaded: JavaScript has only one call stack, so it can do only one thing at a time.
  • Synchronous: By default, code is executed in the order it appears.
  • Asynchronous with Event Loop: Non-blocking behavior is achieved via the event loop, which handles callbacks from asynchronous operations (like setTimeout, fetch).
  • Callbacks, Promises, and Async/Await: These are tools to handle asynchronous tasks in JavaScript.
  • Web Workers: Allow for concurrent execution in separate threads, although they require additional setup.

Despite being single-threaded, JavaScript's asynchronous model allows it to handle many tasks efficiently without blocking the execution of other code.

Top comments (0)