πŸ“˜ Blog 6: Asynchronous JavaScript: Callbacks, Promises, and Async/Await


Modern web applications often perform tasks that take time: fetching data from a server, waiting for user actions, or accessing hardware like cameras or files. These tasks must not block the user interface. JavaScript handles this with asynchronous programming, allowing long-running tasks to happen β€œin the background” while the program continues to run.

In this sixth entry of the JavaScript Essentials series, we’ll explore three fundamental concepts of asynchronous JavaScript:

  • Callbacks
  • Promises
  • Async/Await

1. What is Asynchronous Programming?

JavaScript is single-threaded, meaning it processes one command at a time in a single sequence. But with asynchronous programming, JavaScript can:

  • Start a task (like loading data)
  • Continue doing other things
  • Handle the task’s result when it’s ready

This is crucial for responsive websites and apps.


2. The JavaScript Event Loop (Simplified)

The event loop is a core part of the JavaScript engine.

  • Call Stack – Executes functions line by line.
  • Web APIs – Browser-handled tasks like setTimeout, fetch, etc.
  • Callback Queue – Stores callbacks from Web APIs to run after the stack is empty.

Example:

console.log("Start");
 
setTimeout(() => {
  console.log("Timeout done");
}, 0);
console.log("End");

Output:

Start
End
Timeout done

The setTimeout callback is delayed until the main thread is free.


3. Callbacks

A callback is a function passed to another function to be executed later.

function fetchData(callback) {
  setTimeout(() => {
    console.log("Data fetched");
    callback();
  }, 1000);
}
 
fetchData(() => {
  console.log("Processing data...");
});

πŸ”₯ Callback Hell

When many callbacks are nested:

login(user, function() {
  getProfile(user, function(profile) {
    getPosts(profile.id, function(posts) {
      display(posts);
    });
  });
});

This is hard to read, maintain, and debug β€” known as β€œcallback hell.”


4. Promises

A Promise represents the result of an asynchronous operation.

States:

  • pending – not yet resolved or rejected
  • fulfilled – resolved successfully
  • rejected – operation failed

Creating a Promise:

const promise = new Promise((resolve, reject) => {
  let success = true;
  if (success) resolve("Success!");
  else reject("Failure");
});

Using a Promise:

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

5. Chaining Promises

You can chain .then() calls for multiple asynchronous operations.

getUser()
  .then(user => getProfile(user.id))
  .then(profile => getPosts(profile.id))
  .then(posts => display(posts))
  .catch(error => console.error("Error:", error));

Each .then() waits for the previous one to finish.


6. Fetching Data with Promises

fetch("https://api.example.com/data")
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error("Fetch error:", error));

7. Async/Await – Modern and Clean

async functions return a promise.
await pauses the execution until the promise resolves.

async function getData() {
  try {
    const response = await fetch("https://api.example.com/data");
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error("Error fetching data:", error);
  }
}
 
getData();

βœ… Benefits of Async/Await

  • Cleaner, linear syntax
  • Easier to read
  • Simplifies error handling using try...catch

8. Running Async Tasks in Parallel

async function loadBoth() {
  const [user, profile] = await Promise.all([
    fetchUser(),
    fetchProfile()
  ]);
  console.log(user, profile);
}

9. Delaying Execution with Promises

function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}
 
async function greetLater() {
  await delay(1000);
  console.log("Hello after 1 second");
}

10. Handling Errors Gracefully

async function fetchData() {
  try {
    let res = await fetch("/api/data");
    if (!res.ok) throw new Error("Server error");
    let data = await res.json();
    return data;
  } catch (e) {
    console.error("Failed:", e);
  }
}

11. Using .finally()

fetch("/data")
  .then(data => console.log(data))
  .catch(error => console.error(error))
  .finally(() => console.log("Done loading"));

12. Async Iteration with for await...of

async function fetchMultiple() {
  const urls = ["/data1", "/data2"];
  for await (const url of urls.map(fetch)) {
    const res = await url.json();
    console.log(res);
  }
}

13. Practical Example: Submitting a Form

form.addEventListener("submit", async (e) => {
  e.preventDefault();
  try {
    const response = await fetch("/submit", {
      method: "POST",
      body: new FormData(form),
    });
    const result = await response.json();
    alert("Form submitted!");
  } catch {
    alert("Failed to submit form.");
  }
});

14. Common Pitfalls to Avoid

❌ Forgetting await
❌ Nesting await inside loops needlessly
❌ Ignoring error handling
❌ Mixing callbacks and promises incorrectly


15. Summary Table

| Pattern | Syntax Type | Error Handling | Readability |
|——————|——————–|————————|—————–|\n| Callback | Nested Functions | Manual | ❌ Low |\n| Promise | .then()/.catch()| Built-in | βœ… Medium |\n| Async/Await | async/await | try...catch | βœ…βœ… High |


Conclusion

Asynchronous JavaScript is essential for building fast, non-blocking applications. Understanding callbacks, promises, and async/await will help you handle data fetching, delays, animations, and user interactions effectively.

In the next blog, we’ll dive into JavaScript Modulesβ€”how to organize and reuse your code cleanly using import and export.

Would you like me to begin that one next?

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top