Asynchronous vs Synchronous Programming (What / Why / How)

Introduction

In programming, code can run in two ways: synchronously or asynchronously. These are often shortened to sync and async. In JavaScript, we work with both. By default, JavaScript runs code synchronously, like many other languages. However, as JavaScript has grown in popularity over the past two decades, new features have been added to make it easier to write asynchronous code when needed.

You might not fully grasp the point yet, but by the end of this article, you’ll have a solid understanding of both concepts. We’ll explore them by covering the what, why, and how, with visual and interactive examples.

Let’s get started! Please fasten your seatbelt for a slightly longer but safer drive.

What are Sync and Async?

There is a clear difference between the two. In synchronous programming, JavaScript executes each line from top to bottom, and it will execute the next line only once the previous one has successfully finished. In asynchronous programming, things work differently: if a line takes extra time to finish, JavaScript will ignore it and proceed with the next line. This approach can significantly improve application performance, making it much faster and more responsive. 🤩

Let’s start with sync:

Synchronous

Now that we’ve briefly learned about synchronous programming, it's time to understand it more deeply with an example. Synchronous code runs sequentially from top to bottom. If any line takes extra time to execute, JavaScript will wait for it to finish before moving on to the next line. Let’s understand this with an example:

Sync Email Sender

Have you seen the example above? When you clicked the Send Emails button, it started sending emails from the first user to the last, and finally completed the operation. This is how synchronous programming works: each new task starts only after the previous one has finished.

Now, let’s understand that the next operation or line will not start executing until the previous one is complete. Check out the interactive example below to see this in action:

Async Email Sender with Delay on User 2

Did you notice that when you clicked the Send Emails button, the first email was sent to user1, but it took longer than usual for user2? Due to the delay with user2, the entire program became stuck, and it only sent the email to user3 after finishing with user2, and so on.

Have you realized that this is an issue with synchronous programming, which cannot handle such operations as efficiently as asynchronous programming? We will discuss their merits later in this article, but so far, you can understand how synchronous programming works.

Finally, check out this real-life example to clear any remaining doubts:

Now imagine ordering food at a restaurant, but instead of doing anything else, you just stand at the counter doing nothing — not moving, not talking, not even blinking — until your food is ready. Only after it’s served do you go back to your seat. That’s how synchronous tasks work: everything else has to wait until the current task is finished

Asynchronous

Now that we understand synchronous programming, it’s easier to grasp asynchronous programming, because async is the opposite of sync. In synchronous programming, each line of code executes one after another, with each line depending on the previous block of code. Unlike synchronous programming, in asynchronous programming, each line or block of code runs independently, on its own timeline, without causing delays or blocking the execution of other parts of the program.

Look at the example below — you’ll understand it quickly.

Async Email Sender with Delay on User 2

Have you seen the example above? When you click the button, the emails start sending to users. The emails to user1 and user3 are sent without any delay, while user2 takes longer than usual. However, this delay does not block the execution of the email to user3. As you’ve seen, each user's email is handled independently. That’s why the program doesn’t get stuck, and everything runs smoothly.

Now, imagine a scenario where we need to send hundreds or even thousands of emails. Which would be the better choice — synchronous or asynchronous programming? Obviously, asynchronous, because it sends emails faster without unnecessary delays. Each email task runs independently. In contrast, synchronous programming processes each email one after another. So, if the second email is delayed or fails, all the remaining emails are blocked and won’t be sent.

But that doesn’t mean synchronous programming is useless — in fact, it's still extremely valuable. That’s why most programming languages use synchronous execution by default. If synchronous programming had no value, languages wouldn’t be built around it. We’ll discuss this further in the next section. However, in certain situations, synchronous code is less effective — and that’s where asynchronous programming becomes important.

That said, in today’s age, each programming language has its own methods for handling async tasks whenever needed. In JavaScript, functions like setTimeout, fetch, and event listeners are supported.

I hope you understand these concepts. Now, it’s time to clearly explore their value in detail — so you can see their worth and not assume one is always better or more useful than the other.

Check out this real-life analogy to clarify your understanding doubts.

Imagine ordering food at a restaurant. You place your order (start async task), and then you check your phone, chat, or drink water (other tasks). You don’t stand frozen until the food arrives — that’s the difference between async and sync.

Why it Matters

Let’s start with synchronous programming — where it's useful, and where it becomes ineffective or even a bad choice. I’ll explain with an example:

Why Use Synchronous Programming?

Here, I’ll try to build a simple app that fetches data from an online API to update the UI — but instead of using asynchronous code (which is the standard approach), I’ll use a synchronous method to complete the task.

Now, you might be wondering: why would I do that?

Well, I want to show what happens when we force synchronous logic into a task that’s naturally asynchronous — like calling an API. Since API requests can take time (due to internet speed, server response time, etc.), handling them synchronously could block the entire program and freeze the UI until the response comes back.

This will help you understand why async exists and why sync isn’t suitable for everything — especially when dealing with time-consuming operations like network calls.

Sync API Call with Timer

Timer: 0 seconds

Have you noticed in the example above, I used XMLHttpRequest, which is a synchronous method we used in JavaScript before modern asynchronous techniques (like fetch or async/await) became popular for fetching data from APIs?

Since we’re dealing with an external resource over the network, it naturally takes some time to fetch the data. During this time, synchronous code blocks the main thread.

So, when you clicked the Fetch Data Synchronously button, you probably noticed that the timer — which was updating smoothly — froze for a few seconds. The whole app also froze until the API request was completed.

This behavior clearly demonstrates one of the key problems with synchronous programming in JavaScript: it blocks the UI and user interactions, making it unsuitable for time-consuming operations like network requests.

Now you've probably understood that we can't handle everything using a synchronous approach — and that’s exactly why async programming came into the picture.

But you might still be wondering: Then why don't we just use async for everything, instead of sync, since async feels simpler and runs independently?

If that question popped into your mind — great! 🤔

The truth is, while async programming is powerful (especially for time-consuming operations like API calls or file access), it’s also more complex to write, debug, and manage. That’s why fully relying on it for everything isn’t always the best approach — it can quickly make your codebase overwhelming and messy. 🤦‍♂️

In programming, we care about both speed and efficiency. Synchronous code is often faster and cleaner for tasks that don’t require waiting — like math, logic, or DOM updates. But when we’re dealing with external tasks (like APIs), those naturally take time — and if handled synchronously, they freeze the entire app.

So, to avoid making your app unresponsive, we use async techniques that let the program continue running while waiting in the background.

If there were a way to handle external tasks synchronously without freezing the UI, we’d definitely choose that — because it's easier to write and understand. But since that’s not possible, we take the complex but effective async path to protect our app and users' experience. 😍

That said, synchronous programming is still incredibly valuable, and we only use the asynchronous approach when it's truly necessary — particularly in situations where the synchronous approach isn't sufficient, like handling external or time-consuming tasks.

So now, I believe you clearly understand why synchronous programming still matters. And if you're still wondering, just remember this:

  • ✅ We use synchronous programming by default.
  • ✅ We switch to async only when a task demands it — like fetching data, waiting for user input, or handling slow operations.

In short: use sync where it works, and async where it’s required. That balance is what keeps our code both clean and responsive. 💡

Why Use Asynchronous?

As we discussed earlier, this is used for external tasks or situations in which we need to introduce a specific delay, such as waiting for a particular operation to complete. Now, we will explore its practical value through different examples and scenarios. I’d like to start with the example above, where I fetched users from an API in a synchronous manner, causing the timer to freeze.

Async API Call with Timer and Delay

Timer: 0 seconds

Have you noticed that when you click the button, the timer starts, and while the API is being fetched (a process that takes some time), the timer continues running without freezing? This demonstrates the power of the async approach over sync. Our app feels smoother, as the screen doesn't freeze, unlike with the synchronous method.

This example demonstrates how tasks that are not effectively handled using synchronous methods can be managed more efficiently with asynchronous ones. I used this example to help you clearly understand the difference between the two approaches—async and sync—and to appreciate the value of both.

So far, we’ve looked at an example involving an external operation (an API call), but I want to emphasize that async is not limited to external tasks. It is also widely used for internal operations, such as introducing delays, scheduling events to occur at a specific time, and more. I understand this might be a bit confusing right now—but don’t worry, it will become clearer as we go through more examples.

Take a look at the example below:

Internal Async Task (Non-blocking Delay)

Click the button to start an internal delay. The timer keeps running without freezing.

Timer: 0 seconds

Have you seen how we added a 3-second delay in our website to stop internal code? After 3 seconds, the line has been printed on the screen. ✅ Delay complete! The UI never froze. And the best thing is that without freezing the website, time itself proves this, as you can see 😀. This wasn’t any external task; it was purely an internal task. You can see in the code below that the given (‘’) line is inside the code.

This is a very powerful concept. Now, I’m going to create a pop-up using this technique. When the user clicks the Popup Me button, the pop-up will appear when the next minute starts, based on their local time.

Popup at Next Minute

Click the button below. The popup will appear exactly at the start of the next minute.

Your Local Time: 8:42:00 AM

Have you seen how we created an effective pop-up using an async approach internally, and how that pop-up activates exactly as the next minute starts? Notice how we added a delay in the code for our pop-up without freezing or slowing down the website. We can also easily make these popups appear after 2, 5, or any specific number of minutes while the user is visiting our website — for example, to promote something or collect emails — all without freezing the site.

Have you understood the value of both approaches? Now it's time to dive into how to practically implement them in code.

How They Work

How Sync Works

As we already know, synchronous code executes from top to bottom. Now, let's demonstrate this using a code example:

JavaScript

copy

console.log("First this should run");

let sum = 0;
for (let i = 0; i < 1e9; i++) {
  sum += i % 10;
}

console.log("Loop finished, sum =", sum);
console.log("Last this should run");

/* Output: 
First this should run
Loop finished, sum = 4500000000
Last this should run
*/

If you run this script on your machine, you will get the following output. However, as it runs, it will first print First this should run, then it will cause a delay. This delay happens because in the for loop, I’ve added i < 1e9, which means it will run 1 billion times.

After the entire loop finishes executing, it prints Loop finished, sum = 4500000000, and immediately after that, the final line Last this should run appears.

I added the sum variable inside the loop to make sure it really runs all 1 billion times. If the loop is empty, modern browsers might skip it because they optimize away code that doesn’t do anything. By including a small operation, we force the loop to execute fully.

So, without any code inside the loop, it won’t work as expected. It might even return undefined or finish instantly, because the browser determines it's unnecessary to run a loop that has no side effects.

You can test this with the example below:

JavaScript

copy

console.log("First this should run");
for (let i = 0; i < 1e9; i++) {
}

console.log("Last this should run");

/* Output: 
First this should run
Last this should run
undefined
*/

If you run this on a machine, it would instantly return the output, without any delay, and instead of a for loop, it would return undefined.

Ah, I’ve slightly drifted off-topic — but it’s always good to understand how code works under the hood.

Anyway, did you notice how the loop completely freezes the operation? That’s because we used a synchronous approach. If we had used a setTimeout to handle this, the operation wouldn’t have been blocked. Instead, the rest of the code would have continued running, and the loop’s result would be handled at the end when it finishes.

Now, let’s use the same example to address the freezing issue using asynchronous techniques — including Promise and setTimeout. I’ll use the same scenario to demonstrate how each one can help avoid blocking the main thread. Let’s start:

How Async Works

I’ll start with setTimeout(). When you call it, JavaScript sets up a timer right away and keeps running the rest of your code — this part is synchronous. Once the timer is done, the callback function you provided runs — that part is asynchronous. This is why setTimeout() is considered part of asynchronous programming.

Let’s look at an example using setTimeout() and see what happens:

JavaScript

copy

console.log("First this should run");

setTimeout(function() {
  // your heavy loop here
  let sum = 0;
  for(let i=0; i<1e9; i++) {
    sum += i % 10;
  }
  console.log("Loop finished, sum =", sum);
}, 0);

console.log("Last this should run");

/* Output: 
First this should run
Last this should run
Loop finished, sum = 4500000000
*/

Notice that the first and second console logs print immediately, as they don’t require extra time. The heavy loop runs afterward, allowing the line Last this should run to execute without freezing.

This demonstrates how setTimeout() solves the problem of blocking by scheduling the heavy work asynchronously. As I mentioned earlier, the setTimeout() call is synchronous but behaves like an asynchronous function because its callback runs later. This is why it’s often referred to as asynchronous, and I have included it in the async section.

But you may be wondering: How does setTimeout() work asynchronously even though it is synchronous?

Well, the key thing to understand is that when you call setTimeout(), the call itself happens immediately and synchronously — meaning JavaScript quickly sets up a timer and moves on without waiting. However, the function you give to setTimeout() (called the callback) doesn’t run right away. Instead, it’s scheduled to run later, after the current code finishes executing and after the timer delay (even if that delay is zero).

So, the synchronous part is just setting up the timer, but the actual work inside the callback runs asynchronously. This is why setTimeout() behaves like an async function even though the call to it is synchronous.

If we try to achieve the same thing using a callback or a promise, we might still need to use setTimeout() inside them to prevent our app from freezing. That’s why I decided not to use them. However, you might wonder: Why do we still need to use setTimeout() with a promise or a callback, even though promises are asynchronous?

Good question. To understand this, we need to look a bit deeper. A promise is asynchronous, which means it doesn’t block the main thread. As the page loads, JavaScript starts executing, and if there is a promise, it runs its internal code immediately, but its .then() part (the result handler) runs later, after the current code finishes. This allows the app to stay responsive. However, sometimes we use setTimeout() to delay some code even more — like when we want it to run in the next cycle of the event loop or simulate a delay. So, it's not that we always need setTimeout(), but in some cases, it's helpful to manage timing and avoid blocking behaviors.

As in our for loop case (for (let i = 0; i < 1e9; i++)), if we try to achieve the same thing using a promise, but without using setTimeout() inside it, the page would still freeze. This is because the for loop is a synchronous operation, and it will block the main thread as soon as it starts running. Wrapping synchronous code in a promise doesn’t make it asynchronous. So, if we try to handle a synchronous operation inside an asynchronous function or a promise without breaking it up, the app can still freeze. To prevent this, we need to use something like setTimeout() to split the work and give the browser a chance to breathe. Check this example below:

JavaScript

copy

console.log("First this should run");

new Promise((resolve, reject) => {
  // Long-running synchronous loop
    let sum = 0;
  for(let i=0; i<1e9; i++) {
    sum += i % 10;
  }

  resolve(sum);
})
.then((sum) => {
  console.log("Loop finished, sum =", sum);
});

console.log("Last this should run");

Did you notice? It behaves just like when we don’t use a promise — the loop still freezes the screen. I already explained the reason earlier. However, we can achieve the same goal more effectively by using setTimeout() inside the promise.

I hope you understand the difference between async and sync, and I bet all of your doubts about this topic have been cleared.

Final Thoughts

Synchronous programming is simple and predictable, but it can block your application when tasks take too long. Asynchronous programming, on the other hand, allows JavaScript to remain responsive by delegating tasks and handling results later. Neither approach is “better” in every case — the key is knowing when to use each.

For quick, sequential operations, synchronous code keeps things straightforward. For tasks involving delays, external resources, or heavy computation, asynchronous techniques like setTimeout, Promises, and async/await are essential.

Mastering both styles will make you a more versatile developer, capable of writing code that’s not only correct but also efficient and user‑friendly.