Web Development /JavaScript Programming / Lesson
Web Development

JavaScript Asynchronous Programming: Callbacks, Promises and async/await

Understand asynchronous JavaScript, callbacks, promises and async/await, and learn how modern web applications handle tasks that take time without blocking the user interface.

🎯

Learning Objectives

["Understand synchronous and asynchronous JavaScript.","Explain why asynchronous programming is important in web applications.","Understand how the JavaScript event loop supports asynchronous behavior.","Identify common asynchronous operations in browsers.","Create and use callback functions.","Understand callback-based asynchronous programming.","Identify problems that can occur with deeply nested callbacks.","Understand the states and lifecycle of a Promise.","Create and consume Promises.","Use then(), catch() and finally() with Promises.","Chain multiple asynchronous operations.","Understand Promise resolution and rejection.","Use async functions to write asynchronous code.","Use await to work with Promises more clearly.","Handle asynchronous errors with try\/catch.","Run multiple asynchronous operations with Promise.all().","Apply asynchronous programming concepts to practical LMS applications."]

📖

Lesson Content

JavaScript Asynchronous Programming: Callbacks, Promises and async/await

Modern websites constantly perform tasks that do not finish immediately. A browser may request information from a server, load an image, wait for a timer, read stored data or communicate with an API.

If JavaScript had to completely stop while waiting for every slow operation to finish, interactive websites would become difficult to use.

JavaScript therefore provides asynchronous programming techniques that allow applications to continue working while waiting for operations to complete.

In this lesson, you will learn three important approaches to asynchronous JavaScript:

  • Callbacks
  • Promises
  • async/await

1. Synchronous JavaScript

Synchronous code generally executes one operation after another.

console.log('First');

console.log('Second');

console.log('Third');

The browser executes the statements in order.

The expected output is:

First
Second
Third

For short operations, this is straightforward and useful.

2. What Is Asynchronous Programming?

Asynchronous programming allows JavaScript to start an operation and continue doing other work while waiting for that operation to finish.

A simple example is a timer.

console.log('Start');

setTimeout(function() {
    console.log('Timer finished');
}, 2000);

console.log('End');

The output will normally be:

Start
End
Timer finished

The timer does not prevent the following statement from executing.

3. Why Asynchronous Programming Matters

Web applications frequently perform tasks that may take an unpredictable amount of time.

Examples include:

  • Fetching information from an API.
  • Loading external resources.
  • Uploading files.
  • Downloading data.
  • Waiting for timers.
  • Reading browser storage.
  • Sending form information to a server.
  • Communicating with a database through a backend service.

Asynchronous programming allows the interface to remain responsive while these operations are being handled.

4. JavaScript and the Event Loop

JavaScript normally executes application code using a single main execution thread in the browser.

Browser features can handle certain asynchronous operations outside the immediate JavaScript execution flow. When an operation is ready, its callback or continuation can be placed into a queue for JavaScript to process.

The event loop coordinates this process.

A simplified model is:

  1. JavaScript executes current code.
  2. An asynchronous operation is started.
  3. The browser handles the waiting work.
  4. JavaScript continues with other available work.
  5. When the asynchronous operation is ready, its continuation is scheduled.
  6. The event loop allows JavaScript to process it when appropriate.

5. setTimeout()

setTimeout() schedules a function to run after a minimum delay.

setTimeout(function() {
    console.log('Hello after two seconds');
}, 2000);

The delay is measured in milliseconds.

1000 milliseconds = 1 second

The delay does not guarantee that the callback will execute at exactly that moment. The callback must wait until JavaScript can process it.

6. Callback Functions

A callback is a function passed to another function so that it can be executed later.

function greet(name, callback) {
    console.log('Hello ' + name);

    callback();
}

greet('Ada', function() {
    console.log('Welcome to Dwallnet.');
});

The anonymous function passed as the second argument is the callback.

7. Callback with setTimeout()

A common asynchronous callback example is:

function loadMessage(callback) {
    setTimeout(function() {
        callback('Data loaded successfully.');
    }, 1000);
}

loadMessage(function(message) {
    console.log(message);
});

The callback runs after the asynchronous operation completes.

8. Callback Data

A callback can receive information produced by the asynchronous operation.

function getStudent(callback) {

    setTimeout(function() {

        const student = {
            name: 'Grace',
            course: 'Web Development'
        };

        callback(student);

    }, 1000);
}

getStudent(function(student) {
    console.log(student.name);
});

This pattern was commonly used in older JavaScript applications.

9. Callback Success and Error

Traditional callback APIs sometimes use a pattern where an error is passed as the first argument and successful data as the second.

function loadStudent(callback) {

    setTimeout(function() {

        const success = true;

        if (!success) {
            callback(new Error('Unable to load student.'), null);
            return;
        }

        callback(null, {
            name: 'Grace',
            course: 'Web Development'
        });

    }, 1000);
}

The calling code can then inspect the error.

loadStudent(function(error, student) {

    if (error) {
        console.error(error.message);
        return;
    }

    console.log(student.name);
});

10. Callback Nesting

When one asynchronous operation depends on another, callbacks can become nested.

loadStudent(function(error, student) {

    if (error) {
        return;
    }

    loadCourse(student, function(error, course) {

        if (error) {
            return;
        }

        loadLessons(course, function(error, lessons) {

            if (error) {
                return;
            }

            console.log(lessons);

        });

    });

});

As the number of dependent operations increases, deeply nested callbacks can become harder to read and maintain.

11. Callback Hell

The informal term callback hell describes code that becomes excessively nested because many asynchronous operations depend on one another.

The problem is not that callbacks are inherently bad. Callbacks remain useful in many situations. The issue is excessive nesting and complexity.

12. Introducing Promises

A Promise represents the eventual result of an asynchronous operation.

A Promise can be:

  • Pending — the operation has not completed.
  • Fulfilled — the operation completed successfully.
  • Rejected — the operation failed.

Once a Promise is fulfilled or rejected, it is settled.

13. Creating a Promise

A Promise can be created with the Promise constructor.

const promise = new Promise(function(resolve, reject) {

    const success = true;

    if (success) {
        resolve('Operation successful.');
    } else {
        reject(new Error('Operation failed.'));
    }

});

The resolve() function represents success.

The reject() function represents failure.

14. Consuming a Promise with then()

The then() method handles a fulfilled Promise.

promise.then(function(result) {
    console.log(result);
});

This allows the application to process the successful result.

15. Handling Promise Errors with catch()

The catch() method handles rejection.

promise
    .then(function(result) {
        console.log(result);
    })
    .catch(function(error) {
        console.error(error.message);
    });

This creates a clear separation between successful and failed outcomes.

16. finally()

The finally() method runs after a Promise settles, regardless of whether it was fulfilled or rejected.

promise
    .then(function(result) {
        console.log(result);
    })
    .catch(function(error) {
        console.error(error.message);
    })
    .finally(function() {
        console.log('Operation completed.');
    });

This is useful for cleanup operations such as hiding a loading indicator.

17. Returning Values from then()

A Promise chain can pass values from one step to the next.

Promise.resolve(10)
    .then(function(value) {
        return value * 2;
    })
    .then(function(value) {
        return value + 5;
    })
    .then(function(value) {
        console.log(value);
    });

Each then() can return a value for the next step.

18. Returning Another Promise

A then() callback can also return another Promise.

function getStudent() {
    return new Promise(function(resolve) {

        setTimeout(function() {
            resolve({
                name: 'Grace'
            });
        }, 1000);

    });
}

getStudent()
    .then(function(student) {
        console.log(student.name);
    });

This makes it possible to create chains of asynchronous operations.

19. Promise Chaining

Suppose an LMS needs to:

  1. Load a student.
  2. Load the student’s course.
  3. Load the course lessons.

Promises can represent this workflow without deeply nesting callbacks.

getStudent()
    .then(function(student) {
        return getCourse(student);
    })
    .then(function(course) {
        return getLessons(course);
    })
    .then(function(lessons) {
        console.log(lessons);
    })
    .catch(function(error) {
        console.error(error);
    });

20. Promise.resolve()

Promise.resolve() creates a fulfilled Promise from a value.

Promise.resolve('Hello')
    .then(function(message) {
        console.log(message);
    });

This is useful when working with APIs that may return either direct values or Promise-based results.

21. Promise.reject()

Promise.reject() creates a rejected Promise.

Promise.reject(
    new Error('Something went wrong.')
)
.catch(function(error) {
    console.error(error.message);
});

22. async Functions

An async function always returns a Promise.

async function greet() {
    return 'Hello student';
}

Because the function is asynchronous, its returned value can be consumed as a Promise.

greet().then(function(message) {
    console.log(message);
});

23. The await Keyword

The await keyword can be used inside an async function to wait for a Promise to settle before continuing that async function.

async function loadStudent() {

    const student = await getStudent();

    console.log(student);

}

This syntax often makes asynchronous code easier to read because the sequence of operations appears more like normal synchronous code.

24. async/await Example

function getStudent() {

    return new Promise(function(resolve) {

        setTimeout(function() {

            resolve({
                name: 'Grace',
                course: 'Web Development'
            });

        }, 1000);

    });
}

async function displayStudent() {

    const student = await getStudent();

    console.log(student.name);
    console.log(student.course);

}

displayStudent();

25. Handling Errors with try/catch

Asynchronous errors can be handled inside an async function using try and catch.

async function loadStudent() {

    try {

        const student = await getStudent();

        console.log(student);

    } catch (error) {

        console.error(error.message);

    }

}

This creates a familiar error-handling structure for asynchronous operations.

26. finally with async/await

A finally block can be used when cleanup should happen regardless of success or failure.

async function loadStudent() {

    try {

        const student = await getStudent();

        console.log(student);

    } catch (error) {

        console.error(error.message);

    } finally {

        console.log('Loading finished.');

    }

}

27. Sequential Asynchronous Operations

Sometimes one operation depends on the result of another.

async function loadDashboard() {

    const student = await getStudent();

    const course = await getCourse(student);

    const lessons = await getLessons(course);

    console.log(student);
    console.log(course);
    console.log(lessons);

}

Each operation waits for the previous one because the next operation requires its result.

28. Parallel Asynchronous Operations

Sometimes multiple operations do not depend on one another.

For example, a dashboard may need:

  • Student profile information.
  • Course announcements.
  • Recent notifications.

If these operations are independent, waiting for each one sequentially may be unnecessary.

Promise.all() can wait for several Promises together.

const results = await Promise.all([
    getStudent(),
    getAnnouncements(),
    getNotifications()
]);

The results are returned as an array in the same order as the supplied Promises.

29. Promise.all()

Example:

async function loadDashboard() {

    const [
        student,
        announcements,
        notifications
    ] = await Promise.all([
        getStudent(),
        getAnnouncements(),
        getNotifications()
    ]);

    console.log(student);
    console.log(announcements);
    console.log(notifications);

}

This is an important pattern for modern dashboards and web applications.

30. Promise.all() and Errors

Promise.all() rejects when one of the supplied Promises rejects.

try {

    const results = await Promise.all([
        getStudent(),
        getCourse(),
        getLessons()
    ]);

    console.log(results);

} catch (error) {

    console.error(error);

}

If you need independent handling of each operation’s result, other Promise methods may be more appropriate.

31. Promise.allSettled()

Promise.allSettled() waits for all supplied Promises to settle, whether they succeed or fail.

const results = await Promise.allSettled([
    getStudent(),
    getCourse(),
    getLessons()
]);

console.log(results);

This can be useful when an application wants to inspect every operation rather than stopping when one fails.

32. Loading Indicators

Asynchronous operations often require a loading indicator.

async function loadDashboard() {

    showLoading();

    try {

        const data = await getDashboardData();

        displayDashboard(data);

    } catch (error) {

        displayError(error);

    } finally {

        hideLoading();

    }

}

The finally block ensures that the loading indicator can be removed after completion.

33. Asynchronous Form Submission

A form can use asynchronous JavaScript to send information to a server.

form.addEventListener('submit', async function(event) {

    event.preventDefault();

    try {

        const formData = new FormData(form);

        const result = await submitFormData(formData);

        console.log(result);

    } catch (error) {

        console.error(error);

    }

});

This approach is common in modern applications and will connect naturally with the Fetch API in the next lesson.

34. Async Arrow Functions

Arrow functions can also be asynchronous.

const loadStudent = async () => {

    const student = await getStudent();

    return student;

};

The same Promise-based behavior applies.

35. Await and Non-Promise Values

await is primarily used with Promises, but JavaScript can also accept a non-Promise value.

async function example() {

    const value = await 100;

    console.log(value);

}

The value is treated as an already available result.

36. Avoiding Unnecessary await

Not every operation needs to be awaited immediately.

For independent operations, starting them first and waiting for them together can be more efficient.

Instead of:

const student = await getStudent();
const courses = await getCourses();
const announcements = await getAnnouncements();

when all three are independent, consider:

const studentPromise = getStudent();
const coursesPromise = getCourses();
const announcementsPromise = getAnnouncements();

const [
    student,
    courses,
    announcements
] = await Promise.all([
    studentPromise,
    coursesPromise,
    announcementsPromise
]);

37. Sequential vs Parallel Operations

Use sequential execution when a later operation depends on an earlier result.

const student = await getStudent();
const course = await getCourse(student);

Use parallel execution when operations are independent.

const [student, courses] = await Promise.all([
    getStudent(),
    getCourses()
]);

Understanding this distinction helps developers build responsive applications.

38. Asynchronous Programming in an LMS

A modern LMS may perform several asynchronous operations when a student opens a dashboard.

For example:

  • Load the student’s profile.
  • Load enrolled courses.
  • Load lesson progress.
  • Load notifications.
  • Load recent assignments.

These operations may communicate with different server endpoints.

JavaScript can coordinate these operations while keeping the page interactive.

39. Practical LMS Example

function getStudentProfile() {
    return Promise.resolve({
        name: 'Grace',
        course: 'Web Development'
    });
}

function getProgress() {
    return Promise.resolve({
        completed: 14,
        total: 22
    });
}

async function loadStudentDashboard() {

    try {

        const [
            student,
            progress
        ] = await Promise.all([
            getStudentProfile(),
            getProgress()
        ]);

        console.log(
            'Student:',
            student.name
        );

        console.log(
            'Progress:',
            progress.completed,
            '/',
            progress.total
        );

    } catch (error) {

        console.error(
            'Dashboard loading failed:',
            error
        );

    }

}

loadStudentDashboard();

40. Common Mistakes

  • Assuming asynchronous operations always finish in the order they started.
  • Forgetting to handle rejected Promises.
  • Creating unnecessary callback nesting.
  • Using await outside an appropriate async context.
  • Running independent operations sequentially without a reason.
  • Forgetting to display a loading or error state when appropriate.
  • Assuming setTimeout() executes at exactly the requested millisecond.
  • Using Promise.all() when individual failures should be handled independently.
  • Ignoring errors from asynchronous operations.

41. Best Practices

  • Use Promises for structured asynchronous workflows.
  • Prefer async/await when it makes the control flow easier to understand.
  • Always plan how rejected Promises will be handled.
  • Use try/catch around important asynchronous operations.
  • Use Promise.all() for independent operations when appropriate.
  • Use sequential await when operations depend on one another.
  • Provide useful loading and error states.
  • Keep asynchronous functions focused on specific responsibilities.
  • Do not assume network operations will always succeed.
  • Keep user interfaces responsive while asynchronous work is occurring.

42. Practical Exercise

Create three simulated asynchronous functions:

  1. getStudent()
  2. getCourse()
  3. getProgress()

Each function should return a Promise.

Then create an async function called loadDashboard() that:

  • Loads the student information.
  • Loads the course information.
  • Loads progress information.
  • Uses try/catch for error handling.
  • Displays a loading message before the operation starts.
  • Displays a completion message inside finally.

43. Mini Challenge

Assume the following functions return Promises:

getProfile()
getCourses()
getNotifications()

Write an async function that loads all three resources as efficiently as possible when they are independent.

Use Promise.all() and destructuring to store the results.

44. Connection to the Next Lesson

Asynchronous JavaScript becomes especially powerful when combined with the Fetch API.

The Fetch API allows browser applications to communicate with web servers and APIs using modern Promise-based JavaScript.

In the next lesson, you will learn how to use fetch(), JSON and external data to build applications that communicate with real web services.

45. Lesson Summary

Asynchronous programming is essential for modern web development because many browser operations take time to complete.

Callbacks provide a way to execute functions after operations finish. Promises provide a structured representation of future results and make asynchronous workflows easier to organize. Async functions and await provide a readable way to work with Promises.

Understanding when to run operations sequentially and when to run independent operations together is also an important practical skill.

These concepts form the foundation for API communication, dynamic dashboards, asynchronous forms and modern JavaScript applications.

💻

Code Example

function getStudent() {
    return new Promise(function(resolve) {

        setTimeout(function() {
            resolve({
                name: 'Grace',
                course: 'Web Development'
            });
        }, 1000);

    });
}

function getProgress() {
    return new Promise(function(resolve) {

        setTimeout(function() {
            resolve({
                completed: 14,
                total: 22
            });
        }, 800);

    });
}

function getNotifications() {
    return new Promise(function(resolve) {

        setTimeout(function() {
            resolve([
                'New assignment available',
                'Lesson 15 is now active'
            ]);
        }, 600);

    });
}

async function loadDashboard() {

    console.log('Loading dashboard...');

    try {

        const [
            student,
            progress,
            notifications
        ] = await Promise.all([
            getStudent(),
            getProgress(),
            getNotifications()
        ]);

        console.log('Student:', student.name);
        console.log(
            'Course:',
            student.course
        );

        console.log(
            'Progress:',
            progress.completed + '/' + progress.total
        );

        console.log(
            'Notifications:',
            notifications
        );

    } catch (error) {

        console.error(
            'Dashboard loading failed:',
            error
        );

    } finally {

        console.log('Dashboard loading completed.');

    }
}

loadDashboard();
⭐

Key Takeaways

["Synchronous JavaScript normally executes statements in sequence.","Asynchronous programming allows applications to handle operations that take time without unnecessarily blocking the interface.","Callbacks are functions passed to other functions for later execution.","Promises represent the eventual result of an asynchronous operation.","A Promise can be pending, fulfilled or rejected.","then() handles fulfilled Promise results, while catch() handles rejection.","finally() can perform cleanup after a Promise settles.","async functions always return Promises.","await makes Promise-based code easier to read inside async functions.","try\/catch can handle errors in async\/await workflows.","Promise.all() is useful when independent asynchronous operations can run together.","Sequential await is appropriate when one operation depends on the result of another.","Asynchronous operations can fail and should always have appropriate error handling.","Loading and error states are important parts of a good asynchronous user experience.","Async JavaScript provides the foundation for working with APIs and the Fetch API."]

Enjoying this lesson?

Sign in to your Dwallnet Academy student account to track your learning progress and mark lessons as complete.

Sign In to Track Progress