11. Async Programming and Promises in TypeScript
Master asynchronous programming in TypeScript! Learn Promises, async/await, error handling, and advanced patterns like AbortController. π
What we will learn in this post?
- π Promises in TypeScript
- π Async/Await Syntax
- π Error Handling in Async Code
- π Async Iteration and Generators
- π Typing Callback Functions
- π Promise Combinators and Utilities
- π AbortController and Cancellation
Understanding Promises in TypeScript π
What are Promises?
In TypeScript, a Promise is like a box that will eventually hold a value. You can think of it as a way to handle things that take time, like fetching data from a server. This prevents the dreaded βcallback hellβ and makes your code cleaner and easier to read. Itβs the standard for modern asynchronous JavaScript and TypeScript development.
Typing Promises
You can type a promise using Promise<T>, where T is the type of value you expect. For example:
1
2
3
let myPromise: Promise<string> = new Promise((resolve, reject) => {
// some async operation
});
Chaining Promises π
You can chain promises using .then() and .catch():
1
2
3
4
5
6
7
myPromise
.then(result => {
console.log(result); // Handle success
})
.catch(error => {
console.error(error); // Handle error
});
Type Inference
TypeScript can often infer the types in promise chains, making your code cleaner!
Working with Multiple Promises β‘
Promise.all(): Waits for all promises to resolve.Promise.race(): Resolves as soon as one promise resolves.
Typing Return Values
You can type the return values of these methods too:
1
let results: Promise<[string, number]> = Promise.all([promise1, promise2]);
graph TD
A["Start"]:::style1 --> B{"Is Data Ready?"}:::style3
B -- "Yes" --> C["Resolve Promise"]:::style6
B -- "No" --> D["Reject Promise"]:::style5
C --> E["Execute .then()"]:::style2
D --> F["Execute .catch()"]:::style5
E --> G["Finish"]:::style4
F --> G
classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style5 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
classDef style6 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
linkStyle default stroke:#e67e22,stroke-width:3px;
Happy coding! π
Understanding Async/Await in TypeScript π
Async/await is a powerful way to handle asynchronous code in TypeScript
Error Handling in Async TypeScript π
Handling errors in async TypeScript can be tricky, but with the right patterns, you can make your code robust and easy to maintain. Letβs explore some friendly tips! π
Understanding Error Types
In TypeScript, you can create custom error classes to handle specific errors. This helps in identifying issues easily.
1
2
3
4
5
6
class NotFoundError extends Error {
constructor(message: string) {
super(message);
this.name = "NotFoundError";
}
}
Using Try/Catch
When working with async functions, use try/catch to handle errors gracefully.
1
2
3
4
5
6
7
8
9
10
11
async function fetchData(url: string) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new NotFoundError("Data not found!");
}
return await response.json();
} catch (error) {
console.error(error);
}
}
Promise Rejection Types
TypeScript allows you to define the type of errors your promises might reject. This makes your error handling type-safe.
1
2
3
4
5
6
7
8
9
type FetchError = NotFoundError | Error;
async function fetchDataWithErrorHandling(url: string): Promise<void | FetchError> {
try {
// Fetch logic...
} catch (error) {
return error as FetchError; // Type assertion
}
}
Creating Type-Safe Utilities
You can create utility functions to handle errors consistently.
1
2
3
4
5
6
7
function handleError(error: FetchError) {
if (error instanceof NotFoundError) {
console.error("Custom Error:", error.message);
} else {
console.error("General Error:", error);
}
}
Introduction to Async Iterators and Generators π
Async iterators and generators are powerful tools in JavaScript that help us work with data that arrives over time, like streaming data or large datasets. They allow us to handle data asynchronously, making our applications more efficient and responsive.
What are Async Iterators? π€
- Async Iterators let you iterate over data that is fetched asynchronously.
- They return an
AsyncIterableIterator<T>, which can be used withfor await...ofloops.
Creating an Async Iterator
Hereβs a simple example of an async iterator that simulates fetching data:
1
2
3
4
5
6
async function* fetchData() {
for (let i = 1; i <= 5; i++) {
await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate delay
yield i; // Yield data
}
}
Consuming an Async Iterator
You can consume this iterator using a for await...of loop:
1
2
3
4
5
(async () => {
for await (const value of fetchData()) {
console.log(value); // Logs 1, 2, 3, 4, 5 with a delay
}
})();
Use Cases π
- Streaming Data: Handle real-time data like live feeds.
- Pagination: Fetch data in chunks, improving performance.
- Processing Large Datasets: Work with data that doesnβt fit in memory.
Async iterators and generators make it easier to work with data that comes in over time, keeping your applications smooth and efficient! π
Understanding Async Patterns in Node.js π
Error-First Callbacks β οΈ
In Node.js, we often use error-first callbacks. This means the first argument of the callback is an error (if any), and the second is the result. Hereβs a simple example using setTimeout:
1
2
3
4
5
6
7
8
9
10
11
12
13
function delay(ms, callback) {
setTimeout(() => {
callback(null, `Waited for ${ms} milliseconds`);
}, ms);
}
delay(1000, (err, result) => {
if (err) {
console.error(err);
} else {
console.log(result);
}
});
Converting Callbacks to Promises π
You can convert callback-based functions to promises using util.promisify. Hereβs how to do it with fs.readFile:
1
2
3
4
5
6
7
8
const fs = require('fs');
const util = require('util');
const readFileAsync = util.promisify(fs.readFile);
readFileAsync('example.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));
Higher-Order Functions ποΈ
You can create functions that accept callbacks. Hereβs an example:
1
2
3
4
5
6
7
8
9
10
11
12
13
function processFile(filePath, callback) {
readFileAsync(filePath, 'utf8')
.then(data => callback(null, data))
.catch(err => callback(err));
}
processFile('example.txt', (err, data) => {
if (err) {
console.error(err);
} else {
console.log(data);
}
});
Understanding Promise Utility Methods π
JavaScript promises are powerful tools for handling asynchronous operations. Letβs explore some useful promise utility methods: Promise.allSettled, Promise.any, Promise.resolve, and Promise.reject.
1. Promise.allSettled β
What it does:
This method takes an array of promises and returns a promise that resolves after all of the given promises have either resolved or rejected.
Return Type:
Promise<Array<{status: string, value?: any, reason?: any}>>
When to use:
Use it when you want to know the outcome of all promises, regardless of whether they succeed or fail.
Example:
1
2
3
4
5
const promises = [Promise.resolve(1), Promise.reject('Error'), Promise.resolve(3)];
Promise.allSettled(promises).then(results => {
console.log(results);
});
2. Promise.any π
What it does:
This method takes an array of promises and returns a promise that resolves as soon as one of the promises in the array fulfills.
Return Type:
Promise<any>
When to use:
Use it when you want the first successful result from multiple promises.
Example:
1
2
3
4
5
const promises = [Promise.reject('Error'), Promise.resolve(2), Promise.resolve(3)];
Promise.any(promises).then(result => {
console.log(result); // Outputs: 2
});
3. Promise.resolve βοΈ
What it does:
This method returns a promise that is resolved with a given value.
Return Type:
Promise<T>
When to use:
Use it to create a resolved promise easily.
Example:
1
2
const resolvedPromise = Promise.resolve('Success!');
resolvedPromise.then(value => console.log(value));
4. Promise.reject β
What it does:
This method returns a promise that is rejected with a given reason.
Return Type:
Promise<T>
When to use:
Use it to create a rejected promise easily.
Example:
1
2
const rejectedPromise = Promise.reject('Failed!');
rejectedPromise.catch(error => console.log(error));
Conclusion π
These promise utility methods help manage asynchronous operations effectively. Use them based on your needs for handling multiple promises, whether you want all results, the first success, or to create resolved or rejected promises.
Using AbortController in TypeScript π
What is AbortController?
AbortController is a built-in JavaScript feature that helps you cancel ongoing async operations, like network requests. This is especially useful when you want to stop a fetch request if it takes too long or if the user navigates away.
Basic Usage
Hereβs how to use it with the fetch API:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const controller = new AbortController();
const signal = controller.signal;
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log(data))
.catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted!');
} else {
console.error('Fetch error:', err);
}
});
// Cancel the request after 5 seconds
setTimeout(() => controller.abort(), 5000);
Custom Async Functions
You can also use AbortSignal in your own async functions:
1
2
3
4
5
6
7
8
9
10
async function fetchData(signal: AbortSignal) {
// Your async logic here
}
const controller = new AbortController();
fetchData(controller.signal).catch(err => {
if (err.name === 'AbortError') {
console.log('Operation cancelled!');
}
});
Timeout Pattern
To implement a timeout, you can use setTimeout to call abort():
1
const timeout = setTimeout(() => controller.abort(), 3000);
Cleanup
Always ensure to clear timeouts to avoid memory leaks:
1
clearTimeout(timeout);
π οΈ Hands-On Assignment: Build a Weather Dashboard
Objective: Create a TypeScript application that fetches weather data for multiple cities concurrently.
Tasks:
- Create an async function
getWeather(city: string)that simulates an API call (usesetTimeout). - Use
Promise.allto fetch data for "London", "New York", and "Tokyo" simultaneously. - Handle potential errors (e.g., if one city fails) using
Promise.allSettled. - Implement a timeout using
AbortControllerto cancel requests if they take longer than 2 seconds.
Challenge: Add a retry mechanism that attempts to fetch data 3 times before failing.
π§ Interactive Quiz
Test your understanding of TypeScript Async Programming!
1. What does `Promise.all` do?
`Promise.all` runs promises in parallel and rejects immediately if *any* of the input promises reject. Use `Promise.allSettled` if you want to wait for all of them regardless of success or failure.
2. Which keyword pauses the execution of an `async` function?
The `await` keyword pauses the execution of an `async` function until the Promise is resolved or rejected.
3. How do you handle errors in an `async/await` function?
In `async/await` syntax, the standard way to handle errors is wrapping the `await` calls inside a `try...catch` block.
4. What is the return type of an `async` function?
An `async` function always returns a `Promise`. If you return a value `T`, it returns `Promise `AbortController` provides a signal that can be passed to asynchronous APIs (like `fetch`) to cancel the operation. 5. What is `AbortController` used for?
# Conclusion Mastering **Async Programming** in TypeScript is essential for building responsive and robust applications. We've covered: * **Promises**: The foundation of async operations. * **Async/Await**: Syntactic sugar for cleaner, readable code. * **Error Handling**: Using `try...catch` and `catch()` blocks. * **Advanced Patterns**: `Promise.all`, `Promise.race`, and `AbortController`. Start applying these patterns in your projects today to handle data fetching, file operations, and timers efficiently! π