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:
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.
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');
Output:
Start
End
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:
- The Call Stack: Where JavaScript keeps track of what function is being executed.
-
Web APIs (or Node APIs in Node.js): Handle tasks that take time (like
setTimeout
,fetch
). - Task Queue: When these tasks are finished, they are pushed into a queue.
- 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');
Output:
Start
End
Inside setTimeout
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');
Output:
Start
End
Promise resolved
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');
Output:
Start
End
Data fetched
The Event Loop in Detail
The event loop is what makes JavaScript non-blocking despite being single-threaded. Here’s a simplified flow:
- When you run code, synchronous tasks are pushed onto the call stack and executed immediately.
- Asynchronous functions (e.g.,
setTimeout
,fetch
) are offloaded to the Web APIs (or Node.js APIs), and JavaScript continues executing other code. - Once these asynchronous operations are complete, the callbacks are pushed to the task queue.
- 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');
Output:
Start
End
Timeout 1
Timeout 2
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');
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 (2)
As an experienced JS dev, this is a very good article that's concise and really helpful to beginners, nice work👌🏼
Thanks