Understanding Synchronous and Asynchronous Programming

Eray Ates
8 min readJun 13, 2024

--

In the dynamic world of programming, understanding the nuances of synchronous and asynchronous programming is crucial. This article delves into these concepts, the creation of asynchronous structures using callbacks, promises, and async/await, and explores API calls with Fetch API, Axios, and Next.js SWR.

Synchronous Programming

Synchronous programming is a programming model where operations take place sequentially. This model is opposite to asynchronous programming. In such a model, operations occur one after the other. The program moves to the next step when the current step has completed execution and has returned an outcome. This linear behavior means that long-running operations are “blocking,” and the program will halt for the duration it takes to complete them.

For instance, consider the following sequence in a restaurant scenario:

Ordering in a restaurant illustration
  • A customer places an order with the waiter.
  • The waiter waits until the kitchen prepares the meal.
  • The waiter then serves the meal to the customer.
  • Only after serving the meal, the waiter can take the next order.

This sequential execution ensures that each step must complete before the next one begins, which can lead to inefficiencies if any step takes a long time to complete.

Characteristics of Synchronous Programming

Synchronous programming follows a single-thread model, meaning it executes one operation at a time in a strict order. While one operation is being performed, other operations’ instructions are blocked. The completion of the first task triggers the next, and so on.

Blocking Architecture: Synchronous programming is a blocking architecture, where each operation must complete before the next one begins. This is suitable for programming reactive systems, where immediate response and order are crucial.

Example Illustration: To illustrate how synchronous programming works, think of a telephone conversation. While one person speaks, the other listens. When the first person finishes, the second tends to respond immediately. This back-and-forth communication exemplifies the blocking nature of synchronous operations.

Pros and Cons

+ Simplicity: The code is easy to understand and follow because each step occurs in a specific order.

+ Predictability: Since operations occur one after the other, the flow of control is straightforward and predictable.

- Inefficiency: Long-running operations can block the entire program, leading to delays and a poor user experience.

- Scalability Issues: The single-threaded nature of synchronous programming can hinder scalability, as it cannot handle multiple tasks simultaneously.

Asynchronous Programming

Asynchronous programming is a method used in computer programming that allows a process to run separately from the main program. This means that a task can begin, run in the background, and notify the main program once it completes, without blocking other tasks from continuing. This approach helps reduce wait times and makes applications more responsive, as the program can continue to perform other operations while waiting for long-running tasks to complete.

How Does Asynchronous Programming Work?

In asynchronous programming, tasks are initiated and then allowed to run in the background. The main program does not wait for these tasks to complete before moving on to other tasks. Once an asynchronous task is finished, it signals the main program, which can then take action based on the result.

For example, consider a web application where a user clicks a button to fetch data from a server:

  1. The application sends a request to the server.
  2. While waiting for the server’s response, the application remains usable, allowing the user to interact with other parts of the app.
  3. When the data is received, the application updates the user interface accordingly.

Key Concepts of Asynchronous Programming

Callbacks: A callback is a function that is passed as an argument to another function and is executed after the first function completes. This is a basic way to handle asynchronous operations but can lead to complex and hard-to-read code if not managed properly. Callback functions are generally used to perform operations such as fetching data from the server, waiting for input from a user, controlling events in the DOM.

Example scenario:

Imagine that you are organising a birthday party.
You need to invite your guests, prepare the cake and plan the games to be played at the party. You also want to invite a clown to entertain the guests. In this case, before inviting a clown to the party, the clown should be included in your event after you have made all the necessary planning, settings and after you have notified that the guests have arrived.
(the clown → callback function, arrival of guests → the function that must be completed before the callback function is called.)

function fetchData(callback) {
setTimeout(() => {
callback('Data retrieved');
}, 1000);
}

fetchData((data) => {
console.log(data);
});

However, when using callback functions, we must be careful not to create the callback hell structure.

Callback Hell: The concept of callback hell is the name given to the complex structure resembling a nested pyramid shape formed by calling asynchronous functions one after the other in callback functions. This makes the code increasingly unmanageable and unreadable.

At this point, promise structures were created to prevent callback hell.

Promises:

Promises are used as a way to handle asynchronous operations in a more organised way. It serves the same purpose as callback functions, but offers many additional capabilities and a more readable syntax. (For example, it does not create structures that make the code difficult to manage like callback hell.)

In JavaScript, a promise is a placeholder for a future value or action. By creating a promise, we tell the JavaScript engine to “promise” to perform a certain action and notify us when it completes or fails.

Illustration of Promise in JavaScript
Creating a promise in JavaScript

A promise begins in the “pending” state, indicating that the operation has not yet completed. From the pending state, a promise can either transition to the “fulfilled” state if the operation is successful, or to the “rejected” state if the operation fails. The “fulfilled” state represents a successful outcome with a resolved value, while the “rejected” state represents a failure with a reason for the rejection. These states help manage asynchronous operations by providing a clear structure for handling success and failure in a consistent manner.

States of a promise

How to consume a promise?

To consume a promise, you need to follow a series of steps. First, you need to create or obtain a reference to the promise, which means you must have the promise assigned to a variable, such as myPromise. This reference is essential as it represents the asynchronous operation whose outcome you are interested in.

Next, you need to attach callbacks to this promise to handle its eventual outcome. This is done using the .then() and .catch() methods. The .then() method is used to specify what should happen when the promise is fulfilled (i.e., the operation is successful). You provide a function to .then() that will be executed with the promise's resolved value. The .catch() method, on the other hand, is used to handle the case where the promise is rejected (i.e., the operation fails). You provide a function to .catch() that will be executed with the reason for the rejection.

Finally, you simply wait for the promise to either be fulfilled or rejected. This waiting is inherent to the asynchronous nature of promises; you do not need to write any additional code to wait, as the callbacks provided to .then() and .catch() will automatically be invoked once the promise settles into either state.

let myPromise = new Promise((resolve, reject) => {
// Some asynchronous operation
let success = true; // Simulating success or failure
if (success) {
resolve('Operation successful');
} else {
reject('Operation failed');
}
});

myPromise.then((result) => {
console.log(result); // This runs if the promise is fulfilled
}).catch((error) => {
console.error(error); // This runs if the promise is rejected
});

Promise vs. Callback
Readability and Maintainability:

  • Callbacks can become unwieldy with deeply nested structures, making the code hard to follow and debug.

Error Handling:

  • Callbacks require manual error handling within each callback, leading to repetitive code.
  • Promises offer centralized error handling with .catch(), simplifying the process.

Flexibility:

  • Callbacks are more flexible in older JavaScript environments but can lead to less maintainable code.
  • Promises, while requiring modern JavaScript features, offer a more powerful and organized approach to asynchronous programming.

Async / Await:
While promises offer a structured way to handle asynchronous operations, they often come with significant code overhead. Each asynchronous response requires an additional function using .then(), which can make the code verbose and harder to read. Async/await, introduced in ECMAScript 2017, provides a more synchronized and readable way to write asynchronous code.

How Async/Await Works

Async Functions:

  • async is a keyword used to declare a function as asynchronous. An asynchronous function always returns a promise. If the function returns a value, JavaScript automatically wraps it in a promise.
  • Syntax: async function myFunction() { ... }

Await:

  • await is a keyword used inside an async function to pause its execution until a promise is resolved or rejected. It allows the code to wait for the promise to settle before continuing execution.
  • Syntax: let result = await somePromise;

Advantages of Async/Await

  • Improved Readability: Async/await syntax makes asynchronous code look more like synchronous code, which is easier to read and understand.
  • Simplified Error Handling: Errors can be caught using try/catch blocks, making error handling straightforward compared to promise chaining.
  • Reduced Code Overhead: By eliminating the need for multiple .then() calls, async/await reduces code verbosity and complexity.
// Function to simulate an asynchronous operation
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Data retrieved successfully');
}, 2000);
});
}

// Using async/await to handle the asynchronous operation
async function getData() {
try {
console.log('Fetching data...');
let result = await fetchData(); // Wait for fetchData to resolve
console.log(result); // Output: Data retrieved successfully
} catch (error) {
console.error('Error:', error);
}
}

getData();

--

--

No responses yet