Featured resource
Tech Upskilling Playbook 2025
Tech Upskilling Playbook

Build future-ready tech teams and hit key business milestones with seven proven plays from industry leaders.

Learn more
  • Labs icon Lab
  • Core Tech
Labs

Guided: Create and Test Asynchronous Functions with TypeScript and Jest

Master the essentials of writing and testing asynchronous code in TypeScript with this Code Lab. You’ll build real-world async functions, like fetching data and handling user authentication, and use Jest to test them with confidence. Learn how to handle success and failure cases, mock API calls, and write clean, maintainable async tests using async/await and Jest's .resolves/.rejects utilities. Whether you're new to async testing or ready to level up, this guided lab will give you practical experience you can apply immediately.

Labs

Path Info

Level
Clock icon Beginner
Duration
Clock icon 45m
Last updated
Clock icon Sep 15, 2025

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Table of Contents

  1. Challenge

    Introduction

    Welcome to the Guided: Testing Asynchronous JavaScript with Jest Lab

    In this Code Lab, you'll build and expand a Jest test suite for an authentication system. You'll learn how to write unit tests for asynchronous functions, handle resolved and rejected promises, and use Jest's mocking utilities to simulate different behaviors.

    By the end of this lab, you'll have practical experience testing asynchronous JavaScript functions and applying advanced Jest features like mockResolvedValue, mockRejectedValue, and mockImplementation. You'll understand how to verify both success and error cases, ensuring your code is reliable and resilient.

    You'll progress through a series of steps that gradually build your test suite while deepening your understanding of Jest's async testing patterns.


    Step Overview

    Step 2: Building an async Data Fetching Function

    In this step, you’ll implement a simple asynchronous function that simulates fetching data, such as user information or application settings. This will give you hands-on experience with async/await and promises in TypeScript, forming the foundation for working with real-world APIs.

    Step 3: Writing Tests for async Data Fetching

    Here, you’ll write Jest tests to validate the asynchronous data fetching function you created. You’ll learn how to use resolves and rejects matchers to properly test both successful responses and error handling in async functions.

    Step 4: Building an async Authentication Flow

    Next, you’ll expand on your async skills by implementing authentication-style functions, including logging in, retrieving a user profile, and combining them into a higher-level flow. These functions mirror real-world authentication systems, helping you understand how async methods interact in multi-step processes.

    Step 5: Writing Tests for async Authentication

    In this step, you’ll test the authentication functions from Step 4. You’ll write Jest tests that confirm valid credentials return the correct data, invalid credentials throw errors, and token-based profile retrieval works as expected. This reinforces best practices for structuring tests around async workflows.

    Step 6: Exploring Advanced Jest async Utilities

    Finally, you’ll go deeper into Jest’s async testing utilities by using mocks to simulate resolved and rejected values. This step demonstrates how mocking helps isolate and control async behavior, making your tests more reliable and maintainable. You’ll gain practice writing tests that simulate external dependencies like authentication services or APIs.


    What You'll Learn

    • How to write Jest tests for asynchronous JavaScript functions
    • How to use resolves and rejects to handle promise outcomes
    • How to mock async functions with mockResolvedValue and mockRejectedValue
    • How to use mockImplementation for custom mock logic
    • How to structure a test suite that covers both happy-path and error cases

    You're ready to get started building a robust async test suite with Jest!


    Prerequisites

    You should have a basic understanding of JavaScript, including how to write functions and use promises or async/await syntax. No prior Jest experience is required—the lab will guide you step by step.

    Throughout the lab, you'll have the option to run your test suite using the Terminal tab to verify your implementations. All commands should be executed from the workspace directory:

    npm run test
    

    Tip: If you need assistance at any point, you can refer to the solution directory. It contains subdirectories for each of the steps with example implementations.

  2. Challenge

    Building an `async` Data Fetching Function

    Building an Asynchronous Data Fetching Function

    In this step, you’ll begin working with asynchronous programming in TypeScript by creating functions that simulate fetching data from an external source—similar to how real-world applications interact with APIs. This will help you get comfortable with the async and await keywords, as well as the underlying Promise class that powers async operations in JavaScript and TypeScript.

    The functions you create will mimic API behavior by returning results after a short delay, and in some cases, by simulating failure. This mirrors how network requests work in practice—sometimes they succeed, sometimes they fail, and your code needs to handle both situations gracefully.

    By the end of this step, you’ll have built utility functions that can:

    • Return simulated data asynchronously
    • Introduce artificial delays to represent network latency
    • Throw errors to represent failed requests
    • Validate input before returning results

    These utilities will form the foundation for more complex async flows you’ll build later in the lab, such as authentication systems.

    What is Asynchronous Programming?

    Asynchronous programming allows your code to start a task and move on without waiting for it to finish. In JavaScript and TypeScript, this is usually done with Promises. A Promise represents a value that may not be available yet, such as data from an API.

    For example:

    function fetchData(): Promise<string> {
      return new Promise((resolve) => {
        setTimeout(() => resolve("Data loaded!"), 1000);
      });
    }
    
    fetchData().then((result) => console.log(result));
    

    Here, fetchData returns a Promise. The setTimeout simulates a delay, and after 1 second, the Promise resolves with "Data loaded!".

    Using async/await

    The async and await keywords make working with Promises easier by letting you write asynchronous code that looks synchronous:

    async function getData() {
      const result = await fetchData();
      console.log(result);
    }
    

    Here, await pauses execution until the Promise resolves, so result contains "Data loaded!".

    Error Handling in Asynchronous Functions

    Sometimes Promises fail (reject), just like API requests might fail. You can handle these errors with try...catch:

    async function getData() {
      try {
        const result = await fetchData();
        console.log(result);
      } catch (error) {
        console.error("Something went wrong:", error);
      }
    }
    

    This is especially useful when simulating failures, such as when the API is down or input data is invalid.

  3. Challenge

    Writing Tests for `async` Data Fetching

    Implement an Asynchronous Data Fetching Function

    In this step, you’ll begin working with asynchronous code in TypeScript by implementing a mock API function. These functions will simulate real-world network requests, including both successful responses and failure scenarios. This is the foundation for building and testing asynchronous workflows in later steps.

    You’ll start by creating a simulateApiCall function that returns a promise, then expand it to include delay and error handling. Next, you’ll implement fetchUserData, which will use simulateApiCall to return mock user information when given a valid userId. Finally, you’ll add input validation so that invalid requests are rejected with an error.

    Asynchronous functions in TypeScript always return a Promise, and you’ll often use the await keyword to pause execution until the promise resolves or rejects. This makes it easier to write code that reads like synchronous logic while still handling asynchronous operations.

    const result = await simulateApiCall({ message: "hello" });
    console.log(result); // { message: "hello" }
    
    What is Jest?

    Jest is a testing framework that makes it easy to write unit tests for JavaScript and TypeScript projects. A Jest test file is typically organized into:

    • Test suites: Created with describe, these group related tests together
    • Test cases: Created with test (or it), these check specific behaviors of your code
    • Assertions: Created with expect, these state the expected outcomes of a test

    Example:

    describe("Math utilities", () => {
      test("adds two numbers", () => {
        expect(2 + 3).toBe(5);
      });
    });
    
    Testing Asynchronous Functions

    When testing async functions, you often need to use async/await with your test functions:

    test("fetches data successfully", async () => {
      const data = await fetchUserData(1);
      expect(data).toEqual({ id: 1, name: "Alice" });
    });
    

    Here, the await keyword ensures that the test waits for the Promise to resolve before making assertions.

    Using .resolves and .rejects

    Jest also provides helpers for making assertions directly on promises without manually awaiting them. These are especially useful for testing asynchronous success and failure cases.

    For rejected Promises, you can use .rejects with Jest:

    test("fails on invalid userId", async () => {
      await expect(fetchUserData(-1)).rejects.toThrow("Invalid userId");
    });
    

    For resolved Promises, you can use .resolves with Jest:

    test("promise resolves", async () => {
      await expect(simulateApiCall("hello")).resolves.toBe("hello");
    });
    
    Using spyOn and mockRejectedValueOnce

    Sometimes you need to control how a function behaves in a test without changing its actual implementation. Jest provides jest.spyOn to replace a real function with a mock, and methods like mockRejectedValueOnce to simulate failures:

    test("spyOn with mockRejectedValueOnce", async () => {
      const spy = jest.spyOn(api, "simulateApiCall").mockRejectedValueOnce(new Error("Forced failure"));
    
      await expect(api.fetchUserData("123")).rejects.toThrow("Forced failure");
    
      spy.mockRestore(); // clean up after the test
    });
    

    This is a powerful technique when you want to test error handling or edge cases without modifying your production code.

    Why Test Asynchronous Functions?

    Asynchronous code introduces more uncertainty—network calls can fail, inputs can be invalid, and responses can be delayed. By testing async functions thoroughly, you:

    • Confirm that data is returned correctly on success.
    • Ensure that errors are handled properly.
    • Gain confidence that your app behaves correctly even in failure scenarios.

    By the end of this step, you’ll have a fully functional asynchronous data fetching system that lays the groundwork for more complex authentication workflows later in the lab.

  4. Challenge

    Building an `async` Authentication Flow

    Build an Asynchronous Authentication Flow

    In this step, you’ll expand your application by implementing a simple authentication flow. Authentication systems often rely on asynchronous operations, since verifying credentials and fetching user details typically involve calls to external services or databases. Here, you’ll simulate that flow with asynchronous functions written in TypeScript.

    You’ll begin by creating a login function that returns a token when valid credentials are provided. Next, you’ll add token validation logic to getUserProfile, ensuring that only users with a valid token can access their information. Finally, you’ll implement authenticateAndFetchProfile, which combines both steps by handling the login and then immediately fetching the authenticated user’s profile.

    Asynchronous Methods in Authentication

    Most authentication logic in real applications is asynchronous. For example, logging in usually requires making an API request, waiting for the server to respond, and then using a token for further actions. By simulating this with async functions, you’ll practice the same patterns used in real-world systems:

    async function login(username: string, password: string): Promise<string> {
      if (username === "admin" && password === "password") {
        return "mock-token";
      }
      throw new Error("Invalid credentials");
    }
    

    By the end of this step, you’ll have a fully functioning authentication flow that demonstrates how asynchronous methods can be combined to represent real-world login and profile-fetching processes.

  5. Challenge

    Writing Tests for `async` Authentication

    Write Tests for Asynchronous Authentication

    In this step, you’ll focus on verifying the authentication flow you implemented in the previous step. Testing authentication logic is essential to ensure that login, token validation, and profile fetching behave as expected under different conditions. Since these functions are asynchronous, you’ll use Jest’s async testing features to make assertions on promises.

    You’ll begin by importing the authentication functions into a test file, then write a series of tests that cover both successful and failing cases. This includes confirming that valid credentials return a token, invalid credentials throw an error, valid tokens return a profile, and invalid tokens are rejected. You’ll also test the combined authenticateAndFetchProfile function to verify that it succeeds when given correct credentials and fails otherwise.

    Asynchronous Testing Patterns in Jest

    When testing asynchronous functions, you can use async/await with expect(...).resolves for successful outcomes and expect(...).rejects for errors. This allows your tests to pause until the promise resolves or rejects, ensuring accurate assertions:

    test("login succeeds with correct credentials", async () => {
      await expect(login("admin", "password")).resolves.toBe("mock-token");
    });
    
    test("login fails with wrong credentials", async () => {
      await expect(login("user", "wrong")).rejects.toThrow("Invalid credentials");
    });
    

    By the end of this step, you’ll have a comprehensive test suite that validates the full authentication flow, covering both success paths and error handling.

  6. Challenge

    Exploring Advanced Jest `async` Utilities

    Exploring Advanced Jest async Utilities

    In this step, you’ll go deeper into Jest’s testing features by learning how to mock functions. Mocking allows you to replace real implementations with controlled test doubles so you can isolate code under test, simulate specific scenarios, and avoid relying on actual network calls or external dependencies.

    When testing asynchronous code, Jest provides utilities like mockResolvedValue and mockRejectedValue. These let you instantly control the outcome of a promise without writing a custom fake implementation. This is especially useful for testing how your code handles both success and error cases without waiting for real delays:

    const mockApi = jest.fn();
    
    mockApi.mockResolvedValue({ username: "admin" });
    // mockApi() now returns a resolved promise
    
    mockApi.mockRejectedValue(new Error("API failed"));
    // mockApi() now returns a rejected promise
    

    You’ll also explore how to group related tests with describe blocks, and how to use the beforeEach hook to run setup code before every test in a suite. This helps keep your tests clean and reduces duplication when multiple tests share common setup steps:

    describe("mocking examples", () => {
      let mockApi: jest.Mock;
    
      beforeEach(() => {
        mockApi = jest.fn();
      });
    
      test("mock resolved value", async () => {
        mockApi.mockResolvedValue("success");
        await expect(mockApi()).resolves.toBe("success");
      });
    
      test("mock rejected value", async () => {
        mockApi.mockRejectedValue(new Error("failure"));
        await expect(mockApi()).rejects.toThrow("failure");
      });
    });
    

    By the end of this step, you’ll know how to use Jest mocks to simulate different asynchronous outcomes and structure your test suites more effectively.

  7. Challenge

    Conclusion

    Conclusion

    Congratulations on completing the lab!

    You’ve now built and tested a TypeScript application that demonstrates asynchronous programming patterns and modern testing techniques with Jest. Along the way, you:

    • Implemented async functions to simulate API calls and authentication flows
    • Added validation and error handling for more realistic asynchronous behavior
    • Wrote tests for async functions using async/await, .resolves, and .rejects
    • Practiced mocking with Jest to simulate both resolved and rejected promises
    • Organized test suites with describe blocks and improved maintainability with beforeEach hooks

    Together, these skills give you a strong foundation for working with asynchronous code in professional JavaScript and TypeScript projects. You’ve learned not only how to implement async functions, but also how to write reliable, isolated tests that validate both success and failure cases.

    To take your learning further, try extending this lab by mocking additional dependencies, adding more complex validation rules, or introducing external APIs. These practices will help you build resilient applications and gain confidence in your testing workflow.

    Happy coding!

Jaecee is a Contract Author at Pluralsight, specializing in hands-on lab content. With a background in software development, she’s skilled in Ruby on Rails, React, and NLP. She enjoys crafting and spending time with family.

What's a lab?

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Provided environment for hands-on practice

We will provide the credentials and environment necessary for you to practice right within your browser.

Guided walkthrough

Follow along with the author’s guided walkthrough and build something new in your provided environment!

Did you know?

On average, you retain 75% more of your learning if you get time for practice.