Codeskill

Learn to code, step by step

JavaScript Asynchronous Programming: Callbacks, Promises, and Async/Await

Asynchronous JavaScript matters once your pages get past the basics. Callbacks, promises, and async/await – for work that takes time (API calls, timers, file reads) without freezing the page.

What is asynchronous programming?

Asynchronous code lets JavaScript start a slow task and carry on with other work while it waits. The page stays responsive instead of blocking until the task finishes.

Callbacks

A callback is a function passed to another function to be called later. This was the original way to handle async work in JavaScript.

Example – using a callback

function fetchData(callback) {
    setTimeout(() => {
        callback("Here's your data!");
    }, 2000);
}

fetchData(data => {
    console.log(data);  // Logs "Here's your data!" after 2 seconds
});

Callbacks work, but nested callbacks get hard to read – often called ‘callback hell’.

Promises

A Promise represents a value that will arrive later – or an error if something goes wrong. It has three states: pending, resolved, or rejected.

Creating a promise

function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("Data fetched successfully");
        }, 2000);
    });
}

Consuming a promise

fetchData().then(data => {
    console.log(data);  // Logs "Data fetched successfully" after 2 seconds
}).catch(error => {
    console.error(error);
});

Promises flatten nested callbacks into a chain of .then() calls.

Async/await

Async/await (ES2017) is syntax on top of promises. It makes asynchronous code read more like normal sequential code.

Async/await example

async function fetchAndDisplayData() {
    try {
        let data = await fetchData();
        console.log(data);  // Logs "Data fetched successfully" after 2 seconds
    } catch (error) {
        console.error(error);
    }
}

fetchAndDisplayData();

async marks a function as asynchronous. await pauses until a promise resolves.

Handling multiple asynchronous operations

When you need several async tasks at once, Promise.all() runs them in parallel.

Example – Promise.all()

Promise.all([fetchData1(), fetchData2(), fetchData3()])
    .then(([data1, data2, data3]) => {
        console.log(data1, data2, data3);
    })
    .catch(error => {
        console.error(error);
    });

All three promises run at the same time. The results arrive in an array.

Error handling

Always handle errors in async code. Use .catch() with promises, or try...catch with async/await.

Example – error handling with async/await

async function fetchSafeData() {
    try {
        let data = await fetchData();
        console.log(data);
    } catch (error) {
        console.error("Failed to fetch data:", error);
    }
}

fetchSafeData();

Best practices

  1. Avoid callback hell: prefer promises or async/await over deeply nested callbacks.
  2. Error handling: always catch failures in async code.
  3. Keep it readable: if a chain gets long, break it into named functions.
  4. Parallel operations: use Promise.all() when tasks do not depend on each other.

Most real JavaScript apps rely on async code. Learn all three patterns – you will still see callbacks in older code, promises everywhere, and async/await in newer projects.

PreviousForm Validations Using JavaScript: Enhancing User Experience