In this section, we will delve into more complex asynchronous programming patterns in TypeScript. These patterns are essential for building robust and efficient applications that handle multiple asynchronous operations seamlessly.

Key Concepts

  1. Concurrency vs. Parallelism
  2. Promise.all, Promise.race, and Promise.allSettled
  3. Async Iterators
  4. Cancellation of Promises
  5. Error Handling in Complex Async Flows

Concurrency vs. Parallelism

  • Concurrency: Multiple tasks make progress over time. They may not run simultaneously but are managed in a way that they appear to be running at the same time.
  • Parallelism: Multiple tasks run simultaneously, typically on multiple processors or cores.

Example

// Concurrency Example
async function task1() {
    console.log("Task 1 started");
    await new Promise(resolve => setTimeout(resolve, 2000));
    console.log("Task 1 completed");
}

async function task2() {
    console.log("Task 2 started");
    await new Promise(resolve => setTimeout(resolve, 1000));
    console.log("Task 2 completed");
}

async function runTasksConcurrently() {
    await Promise.all([task1(), task2()]);
    console.log("Both tasks completed concurrently");
}

runTasksConcurrently();

Promise.all, Promise.race, and Promise.allSettled

Promise.all

  • Waits for all promises to resolve or any to reject.
const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values); // [3, 42, "foo"]
});

Promise.race

  • Resolves or rejects as soon as one of the promises resolves or rejects.
const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2]).then((value) => {
  console.log(value); // "two"
});

Promise.allSettled

  • Waits for all promises to either resolve or reject.
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promise3 = 42;

Promise.allSettled([promise1, promise2, promise3]).then((results) => {
  results.forEach((result) => console.log(result.status));
  // "fulfilled"
  // "rejected"
  // "fulfilled"
});

Async Iterators

  • Allows asynchronous iteration over data.

Example

async function* asyncGenerator() {
    let i = 0;
    while (i < 3) {
        yield new Promise(resolve => setTimeout(() => resolve(i++), 1000));
    }
}

(async () => {
    for await (const num of asyncGenerator()) {
        console.log(num); // 0, 1, 2
    }
})();

Cancellation of Promises

  • Using AbortController to cancel promises.

Example

const controller = new AbortController();
const signal = controller.signal;

const fetchData = async (url: string) => {
    try {
        const response = await fetch(url, { signal });
        const data = await response.json();
        console.log(data);
    } catch (error) {
        if (error.name === 'AbortError') {
            console.log('Fetch aborted');
        } else {
            console.error('Fetch error:', error);
        }
    }
};

fetchData('https://jsonplaceholder.typicode.com/todos/1');
controller.abort(); // Aborts the fetch request

Error Handling in Complex Async Flows

  • Using try-catch blocks and handling errors in nested async functions.

Example

async function fetchData(url: string) {
    try {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error('Network response was not ok');
        }
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Fetch error:', error);
        throw error;
    }
}

async function processData() {
    try {
        const data = await fetchData('https://jsonplaceholder.typicode.com/todos/1');
        console.log('Data:', data);
    } catch (error) {
        console.error('Process error:', error);
    }
}

processData();

Practical Exercises

Exercise 1: Using Promise.all

Write a function that fetches data from three different URLs concurrently and logs the results.

const urls = [
    'https://jsonplaceholder.typicode.com/todos/1',
    'https://jsonplaceholder.typicode.com/todos/2',
    'https://jsonplaceholder.typicode.com/todos/3'
];

async function fetchAllData(urls: string[]) {
    try {
        const promises = urls.map(url => fetch(url).then(response => response.json()));
        const results = await Promise.all(promises);
        console.log(results);
    } catch (error) {
        console.error('Error fetching data:', error);
    }
}

fetchAllData(urls);

Exercise 2: Using Async Iterators

Create an async generator that yields numbers from 1 to 5 with a delay of 1 second between each number.

async function* numberGenerator() {
    for (let i = 1; i <= 5; i++) {
        yield new Promise(resolve => setTimeout(() => resolve(i), 1000));
    }
}

(async () => {
    for await (const num of numberGenerator()) {
        console.log(num); // 1, 2, 3, 4, 5
    }
})();

Summary

In this section, we explored advanced asynchronous patterns in TypeScript, including concurrency vs. parallelism, Promise combinators, async iterators, cancellation of promises, and error handling in complex async flows. These patterns are crucial for building efficient and robust applications that handle multiple asynchronous operations seamlessly. By mastering these concepts, you will be well-equipped to tackle complex asynchronous programming challenges in your TypeScript projects.

© Copyright 2024. All rights reserved