Skip to main content

Understanding Asynchronous JavaScript and Promises

Updated 3 min readMarkdown

When I first started learning JavaScript, I noticed something confusing. Some lines of code executed immediately, while others ran after a delay.

For example:

console.log("Start");

setTimeout(() => {
  console.log("Inside timeout");
}, 2000);

console.log("End");

Output:

Start
End
Inside timeout

Why does this happen if JavaScript executes code from top to bottom?

Let's understand the concepts behind it.

1. Synchronous vs Asynchronous JavaScript

JavaScript executes synchronous code one statement at a time. Each operation must finish before the next one proceeds.

Asynchronous operations, such as timers and network requests, allow JavaScript to continue executing other code while waiting for a result.

2. Call Stack

The call stack keeps track of the functions currently being executed. It follows the LIFO principle: Last In, First Out.

When a function calls another function, the new function is placed on top of the stack and executes first.

3. Runtime Environment and Callback Queue

The browser or Node.js runtime provides features such as timers, networking, and file operations.

When an asynchronous operation finishes, its callback can be scheduled for execution. The callback must wait until JavaScript can run it.

4. Event Loop

The event loop coordinates the execution of scheduled callbacks and other queued work.

A simplified way to understand it is that synchronous code runs first, Promise callbacks are processed at microtask checkpoints, and timer callbacks run when their scheduled tasks are selected.

Consider this example:

console.log("Start");

setTimeout(() => console.log("Timer"), 0);

Promise.resolve().then(() => console.log("Promise"));

console.log("End");

Output:

Start
End
Promise
Timer

Promise callbacks run as microtasks, which are processed before the next timer task in this example.

5. What Are Promises?

A Promise represents the eventual success or failure of an operation. It has three states:

  • Pending: Still waiting for a result.

  • Fulfilled: Completed successfully.

  • Rejected: Failed.

Example:

const promise = new Promise((resolve, reject) => {
  resolve("Data received");
});

promise.then((data) => {
  console.log(data);
});

Here, resolve() fulfills the Promise, and .then() handles the result.

We can handle errors using .catch():

promise
  .then((data) => console.log(data))
  .catch((error) => console.error(error));

Promises also support chaining, allowing one asynchronous operation to depend on the result of another.

6. Async/Await

JavaScript provides async and await to make Promise-based code easier to read.

async function getData() {
  try {
    const response = await fetch("/api/users");
    const data = await response.json();

    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

An async function returns a Promise, while await suspends that function until the awaited Promise settles. It does not block the entire JavaScript thread.

Conclusion

Asynchronous JavaScript becomes easier to understand when you know how its components work together:

  • Call Stack: Executes JavaScript functions.

  • Runtime Environment: Handles capabilities such as timers and networking.

  • Queues: Hold scheduled work.

  • Event Loop: Coordinates execution of queued work.

  • Promises: Represent eventual results or failures.

  • Async/Await: Makes Promise-based code easier to write and understand.

Understanding these concepts gives you a solid foundation for working with API requests, databases, and other asynchronous operations in JavaScript.

7 views

About the author

  • Building cool stuff in web

No comments yet. Be the first to comment.