Article image Promise and Async/Await in TypeScript

46. Promise and Async/Await in TypeScript

Page 76 | Listen in audio

In modern JavaScript development, handling asynchronous operations is a crucial part of building responsive and efficient applications. TypeScript, being a superset of JavaScript, inherits all of JavaScript's asynchronous capabilities but enhances them with static typing, making asynchronous code more predictable and less error-prone. Two of the most important constructs for managing asynchronous operations in TypeScript are Promises and the async/await syntax.

Promises are a fundamental feature of JavaScript that allow developers to work with asynchronous operations more effectively. A Promise represents a value that may be available now, or in the future, or never. It helps in managing operations that take an indeterminate amount of time to complete, such as network requests or file I/O.

A Promise is essentially an object that can be in one of three states:

  • Pending: The initial state, neither fulfilled nor rejected.
  • Fulfilled: The operation was completed successfully.
  • Rejected: The operation failed.

Here's a basic example of how Promises work in TypeScript:

function fetchData(url: string): Promise<string> {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', url);
        xhr.onload = () => {
            if (xhr.status === 200) {
                resolve(xhr.responseText);
            } else {
                reject(new Error(`Request failed with status ${xhr.status}`));
            }
        };
        xhr.onerror = () => reject(new Error('Network error'));
        xhr.send();
    });
}

fetchData('https://api.example.com/data')
    .then(data => console.log(data))
    .catch(error => console.error(error));

In this example, fetchData is a function that returns a Promise. When the HTTP request is successful, it resolves the Promise with the response data. If the request fails, it rejects the Promise with an error message. The then method is used to handle a successful response, while catch is used to handle errors.

While Promises are powerful, they can lead to "callback hell" when dealing with multiple asynchronous operations. This is where the async/await syntax comes into play. Introduced in ECMAScript 2017, async/await provides a more readable and concise way to work with Promises.

The async keyword is used to declare a function that returns a Promise. Inside an async function, you can use the await keyword to pause the execution of the function until a Promise is resolved. This makes asynchronous code look and behave more like synchronous code, which is easier to read and maintain.

Here's how the previous example can be rewritten using async/await:

async function fetchDataAsync(url: string): Promise<string> {
    const response = await fetch(url);
    if (!response.ok) {
        throw new Error(`Request failed with status ${response.status}`);
    }
    return await response.text();
}

(async () => {
    try {
        const data = await fetchDataAsync('https://api.example.com/data');
        console.log(data);
    } catch (error) {
        console.error(error);
    }
})();

In this example, fetchDataAsync is an async function that uses the Fetch API to make the HTTP request. The await keyword is used to wait for the Promise returned by fetch to resolve. If the request is successful, the response is returned; otherwise, an error is thrown. The try/catch block is used to handle errors, providing a cleaner and more straightforward error-handling mechanism than the then/catch chain.

One of the significant advantages of using async/await in TypeScript is the ability to leverage TypeScript's type system to ensure type safety. For example, when you define the return type of an async function, TypeScript can infer the type of the resolved value, providing better autocompletion and error checking in your development environment.

Consider the following example where TypeScript's type inference works with async/await:

interface User {
    id: number;
    name: string;
    email: string;
}

async function fetchUser(userId: number): Promise<User> {
    const response = await fetch(`https://api.example.com/users/${userId}`);
    if (!response.ok) {
        throw new Error(`Failed to fetch user with id ${userId}`);
    }
    return await response.json();
}

(async () => {
    try {
        const user = await fetchUser(1);
        console.log(`User Name: ${user.name}`);
    } catch (error) {
        console.error(error);
    }
})();

In this example, the fetchUser function fetches user data from an API and returns a Promise that resolves to a User object. TypeScript ensures that the resolved value of the Promise matches the User interface, providing compile-time type checking and reducing runtime errors.

Another benefit of using async/await is the ability to handle multiple asynchronous operations more elegantly. Instead of chaining multiple then calls, you can use await to sequentially execute operations, or use Promise.all to run them concurrently.

Here's an example demonstrating both sequential and concurrent execution with async/await:

async function fetchMultipleUsers(userIds: number[]): Promise<User[]> {
    const users: User[] = [];
    for (const userId of userIds) {
        const user = await fetchUser(userId);
        users.push(user);
    }
    return users;
}

async function fetchUsersConcurrently(userIds: number[]): Promise<User[]> {
    const userPromises = userIds.map(id => fetchUser(id));
    return await Promise.all(userPromises);
}

(async () => {
    const userIds = [1, 2, 3];
    
    // Sequential execution
    const usersSequential = await fetchMultipleUsers(userIds);
    console.log('Sequential:', usersSequential);
    
    // Concurrent execution
    const usersConcurrent = await fetchUsersConcurrently(userIds);
    console.log('Concurrent:', usersConcurrent);
})();

In the fetchMultipleUsers function, users are fetched sequentially, waiting for each request to complete before starting the next. In contrast, the fetchUsersConcurrently function uses Promise.all to fetch all users concurrently, improving performance by reducing the total waiting time.

In conclusion, Promises and async/await are indispensable tools for handling asynchronous operations in TypeScript. While Promises provide a robust mechanism for managing asynchronous tasks, async/await offers a more readable and maintainable syntax. By leveraging TypeScript's type system, developers can write safer and more predictable asynchronous code, ultimately leading to more reliable applications.

Now answer the exercise about the content:

What are the two main constructs for managing asynchronous operations in TypeScript mentioned in the text?

You are right! Congratulations, now go to the next page

You missed! Try again.

Article image Defining Callable Types

Next page of the Free Ebook:

77Defining Callable Types

7 minutes

Earn your Certificate for this Course for Free! by downloading the Cursa app and reading the ebook there. Available on Google Play or App Store!

Get it on Google Play Get it on App Store

+ 6.5 million
students

Free and Valid
Certificate with QR Code

48 thousand free
exercises

4.8/5 rating in
app stores

Free courses in
video, audio and text