- Lab
- Core Tech

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.

Path Info
Table of Contents
-
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
, andmockImplementation
. 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 FunctionIn 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 FetchingHere, you’ll write Jest tests to validate the asynchronous data fetching function you created. You’ll learn how to use
resolves
andrejects
matchers to properly test both successful responses and error handling inasync
functions.Step 4: Building an
async
Authentication FlowNext, 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 howasync
methods interact in multi-step processes.Step 5: Writing Tests for
async
AuthenticationIn 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
UtilitiesFinally, 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 controlasync
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
andrejects
to handle promise outcomes - How to mock
async
functions withmockResolvedValue
andmockRejectedValue
- 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. -
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
andawait
keywords, as well as the underlyingPromise
class that powersasync
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
Promise
s. APromise
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 aPromise
. ThesetTimeout
simulates a delay, and after 1 second, thePromise
resolves with"Data loaded!"
.Using
async
/await
The
async
andawait
keywords make working withPromises
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 thePromise
resolves, soresult
contains"Data loaded!"
.Error Handling in Asynchronous Functions
Sometimes
Promise
s fail (reject), just like API requests might fail. You can handle these errors withtry...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.
-
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 implementfetchUserData
, which will usesimulateApiCall
to return mock user information when given a validuserId
. 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 theawait
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
(orit
), 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 useasync
/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
andmockRejectedValueOnce
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 likemockRejectedValueOnce
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.
- Test suites: Created with
-
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 implementauthenticateAndFetchProfile
, 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.
-
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
withexpect(...).resolves
for successful outcomes andexpect(...).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.
-
Challenge
Exploring Advanced Jest `async` Utilities
Exploring Advanced Jest
async
UtilitiesIn 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
andmockRejectedValue
. 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 thebeforeEach
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.
-
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 usingasync
/await
,.resolves
, and.rejects
- Practiced mocking with Jest to simulate both resolved and rejected promises
- Organized test suites with
describe
blocks and improved maintainability withbeforeEach
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!
- Implemented
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.