Quiz

Explain the difference between synchronous and asynchronous functions in JavaScript

Topics
AsyncJavaScript
Edit on GitHub

TL;DR

Synchronous functions are blocking while asynchronous functions are not. In synchronous functions, statements complete before the next statement is run. As a result, programs containing only synchronous code are evaluated exactly in order of the statements. The execution of the program is paused if one of the statements take a very long time.

function sum(a, b) {
console.log('Inside sum function');
return a + b;
}
const result = sum(2, 3); // The program waits for sum() to complete before assigning the result
console.log('Result: ', result); // Output: 5
// Console output:
// Inside sum function
// Result: 5

Asynchronous functions usually accept a callback as a parameter and execution continue on to the next line immediately after the asynchronous function is invoked. The callback is only invoked when the asynchronous operation is complete and the call stack is empty. Heavy duty operations such as loading data from a web server or querying a database should be done asynchronously so that the main thread can continue executing other operations instead of blocking until that long operation to complete (in the case of browsers, the UI will freeze).

function fetchData(callback) {
setTimeout(() => {
const data = { name: 'John', age: 30 };
callback(data); // Calling the callback function with data
}, 2000); // Simulating a 2-second delay
}
console.log('Fetching data...');
fetchData((data) => {
console.log(data); // Output: { name: 'John', age: 30 } (after 2 seconds)
});
console.log('Call made to fetch data'); // Output: This will be printed first
// Console output:
// Fetching data...
// Call made to fetch data
// { name: 'John', age: 30 }

Synchronous vs asynchronous functions

In JavaScript, the concepts of synchronous and asynchronous functions are fundamental to understanding how code execution is managed, particularly in the context of handling operations like I/O tasks, API calls, and other time-consuming processes.

Synchronous functions

Synchronous functions execute in a sequential order, one after the other. Each operation must wait for the previous one to complete before moving on to the next.

  • Synchronous code is blocking, meaning the program execution halts until the current operation finishes.
  • It follows a strict sequence, executing instructions line by line.
  • Synchronous functions are easier to understand and debug since the flow is predictable.

Synchronous function examples

  1. Reading files synchronously: When reading a file from the file system using the synchronous readFileSync method from the fs module in Node.js, the program execution is blocked until the entire file is read. This can cause performance issues, especially for large files or when reading multiple files sequentially

    const fs = require('fs');
    const data = fs.readFileSync('large-file.txt', 'utf8');
    console.log(data); // Execution is blocked until the file is read.
    console.log('End of the program');
  2. Looping over large datasets: Iterating over a large array or dataset synchronously can freeze the user interface or browser tab until the operation completes, leading to an unresponsive application.

    const largeArray = new Array(1_000_000).fill(0);
    // Blocks the main thread until the million operations are completed.
    const result = largeArray.map((num) => num * 2);
    console.log(result);

Asynchronous functions

Asynchronous functions do not block the execution of the program. They allow other operations to continue while waiting for a response or completion of a time-consuming task.

  • Asynchronous code is non-blocking, allowing the program to keep running without waiting for a specific operation to finish.
  • It enables concurrent execution, improving performance and responsiveness.
  • Asynchronous functions are commonly used for tasks like network requests, file I/O, and timers.

Asynchronous function examples

  1. Network requests: Making network requests, such as fetching data from an API or sending data to a server, is typically done asynchronously. This allows the application to remain responsive while waiting for the response, preventing the user interface from freezing

    console.log('Start of the program'); // This will be printed first as program starts here
    fetch('https://api.example.com/data')
    .then((response) => response.json())
    .then((data) => {
    console.log(data);
    /** Process the data without blocking the main thread
    * and printed at the end if fetch call succeeds
    */
    })
    .catch((error) => console.error(error));
    console.log('End of program'); // This will be printed next before the fetch callback
  2. User input and events: Handling user input events, such as clicks, key presses, or mouse movements, is inherently asynchronous. The application needs to respond to these events without blocking the main thread, ensuring a smooth user experience.

    const button = document.getElementById('myButton');
    button.addEventListener('click', () => {
    // Handle the click event asynchronously
    console.log('Button clicked');
    });
  3. Timers and Animations: Timers (setTimeout(), setInterval()) and animations (e.g., requestAnimationFrame()) are asynchronous operations that allow the application to schedule tasks or update animations without blocking the main thread.

    setTimeout(() => {
    console.log('This message is delayed by 2 seconds');
    }, 2000);
    const animationId = requestAnimationFrame(updateAnimation);

By using asynchronous functions and operations, JavaScript can handle time-consuming tasks without freezing the user interface or blocking the main thread.

It is important to note that async functions do not run on a different thread. They still run on the main thread. However, it is possible to achieve parallelism in JavaScript by using Web workers

Achieving parallelism in JavaScript via web workers

Web workers allow you to spawn separate background threads that can perform CPU-intensive tasks in parallel with the main thread. These worker threads can communicate with the main thread via message passing, but they do not have direct access to the DOM or other browser APIs.

// main.js
const worker = new Worker('worker.js');
worker.onmessage = function (event) {
console.log('Result from worker:', event.data);
};
worker.postMessage('Start computation');
// worker.js
self.onmessage = function (event) {
const result = performHeavyComputation();
self.postMessage(result);
};
function performHeavyComputation() {
// CPU-intensive computation
return 'Computation result';
}

In this example, the main thread creates a new web worker and sends it a message to start a computation. The worker performs the heavy computation in parallel with the main thread and sends the result back via postMessage().

Event loop

The async nature of JavaScript is powered by a JavaScript engine's event loop allowing concurrent operations even though JavaScript is single-threaded. It's an important concept to understand so we highly recommend going through that topic as well.

Further reading

Edit on GitHub