This article was originally written on the Metered Blog: Async/Await in TypeScript: A step by step guide
Async/Await is built on top promises, these offer a more readable and concise way for working with asynchronous operations
Promises represents a future value that can be rejected or resolved, serving as placeholders for the result of async operations
Async/Await simplifies working with promises by providing the synthetic sugar, thus making it easier to work with promises
We can compare promises with traditional callback patterns to better understand the differences
- Callback Patterns: A callback is a function that is passed to another function as an argument. This function is then called by the outer function to perform some kind of operation or a routine action.
If you have a lot of callbacks, this could lead to a lot of deeply nested code which is hard to read or maintain and debug. This is often reffered to as "callback hell"
- Promises: Promises were introduced in order to solve the callback hell problem.
A promise is a JavaScript Object that represents a temporary or an intermediate state in an asynchronous operation. (A promise that is yet to be fulfilled)
So, it is basically a placeholder for the result of an asynchronous operation. Promises allow chaining of operations and have dedicated functions for handing of success (`then`) and failures (`catch`) thus improving readability and error handling.
The Async/Await improves promises by providing a synthetic sugar over promises and makes it easier to write.
Developers can write the code that feels and appears synchronous but operates asynchronously and avoid the nested chaining of callback methods.
Async/ Await: Basic Syntax and usage
- Async Keyword: The
async
keyword returns a promise. When you declare a function, basically write async before the function declaration, then it wraps the function in a promise
If the function throws an error then the promise will be rejected with that error
async function getAllUsers() {
//This function returns a promise
}
- Await Keyword: Within the async function you can use the
await
keyword to pause the execution of the function untill the Promise is resolved.
The await
keyword basically waits for the promise to be resolved and then returns the result of the promoise, which could either be success or failure.
let data = await someAsyncCall();
Simple Examples demonstrating syntax and usage
Example with promise without async/await
function getAllUsers() {
fetch('https://api.example.com/userdata')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error fetching data:", error));
}
Same Example with async/await
async function getAllUsers() {
try {
const response = await fetch('https://api.example.com/userdata');
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Error fetching data:", error);
}
}
In the above example as you can see the async/await code is easy to understand. The await keyword pauses the execution of the getAllUsers()
function until the promise is resolved
We also have the try catch block in order to catch any errors, that could be occurring during the fetch operation.
Error Handling with Async/Await
Error handling is an important aspect of writing code especially when dealing with asynchronous operations.
Here are some of the techniques with which you can handle error in typescript
Try/Catch Blocks: The most easy and common method of catching errors is the try/catch block. Whenever the async operation throws an error, the await function that is waiting for the fulfillment of the promise will throw the rejected values. You can wrap the await expression in a try/catch block and you can handle these errors gracefully.
Error Propagation: You can have flexibility on where you handle the error. This is because the async errors are propagated to the caller if they are not caught inside the function. So, you can handle the error outside the function if that is what you wish.
Finally Block: You can also have a finally block, this is similar to synchronous code. the finally block executes some code regardless of the out code of the async/await.
Examples of Error handling
async function fetchData(url: string) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error("Error fetching data:", error);
}
}
Example 2: Propogating error to a higher level and then catching them
async function fetchData(url: string) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
async function processData() {
try {
const data = await fetchData('https://api.example.com/data');
console.log(data);
} catch (error) {
console.error("Error processing data:", error);
}
}
Parallel vs. Sequential Execution with Async/Await
With Async/Await you have to option to opt for sequential or parrallel execution. This option can be good for optimizing the performance or benefiting from resource utilization
Strategies for managing the execution flow wither Parallel or Sequential
- Sequential execution: When you use the
await
keyword in a loop or use multipleawait
keywords one after the another the code executes in a sequential order.
This is because the await keywords waits for the fulfillment of the promise before moving forward.
Hence, each operation will only start when the previous one has completed. This is important when the execution of one operation depends on the result of the previous one.
- Parallel Execution: You can also run all the async operations in parallel as well and then wait for all of them to complete using the
Promise.all
This is a good idea when all the async operations are independent of each other
Let us consider some example of each of these
Sequential Execution Example
async function sequentialStart() {
const urls = ['url1', 'url2', 'url3'];
for (const url of urls) {
const data = await fetchData(url); // Each fetch operation waits for the previous one to complete
console.log(data);
}
}
async function parallelStart() {
const urls = ['url1', 'url2', 'url3'];
const promises = urls.map(url => fetchData(url)); // Initiating all fetch operations simultaneously
const results = await Promise.all(promises); // Waiting for all fetch operations to complete
console.log(results);
}
We are using the Promise.all
to wait for all the fetch operations to complete in parallel. This is more efficient approach if your Async operations are not dependent on one another.
Best Practices for Using Async/Await with Native Promises
Use async/await for Most Async Code
Promise Chains for Parallel Execution
Handle errors with Try/Catch
Avoid Mixing Callbacks, Promises and Async/Await
Example of how to convert the Promise based code to Async/Await
- Promise based code
function getUser(userId) {
return fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(user => console.log(user))
.catch(error => console.error("Failed to fetch user:", error));
}
Async/await code
async function getUser(userId: string) {
try {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
console.log(user);
} catch (error) {
console.error("Failed to fetch user:", error);
}
}
2. Example with Parallel code execution
- Promise based code
function fetchUserData(userId) {
const userPromise = fetch(`/api/users/${userId}`).then(res => res.json());
const postsPromise = fetch(`/api/users/${userId}/posts`).then(res => res.json());
return Promise.all([userPromise, postsPromise])
.then(([user, posts]) => {
console.log(user, posts);
})
.catch(error => {
console.error("Failed to fetch data:", error);
});
}
2. Async/ Await code
Optimizing Performance: Tips and Tricks
Avoiding unnecessary await: Using await unnecessarily can lead to performance bottlenecks. Whenever possible consider using
Promise.all
Using
Promise.all
for parallel execution whenever feasibleCache the results of async operations: Because async operations take time, this be because they need to acquire data from a server etc. Consider caching data which does not change much
Streamline Error Handling: Consider having a consistant error handling mechanism.
Example of optimizing performance
In this example we are going to improve the API response time. Consider an app where the app needs to fetch data from time to time and from multiple endpoints before rendering data.
This fetching is implemented sequentially which leads to longer load times
async function fetchDataSequentially() {
const userData = await fetchUserData();
const postsData = await fetchPostsData(userData.id);
const commentsData = await fetchCommentsData(postsData.id);
// Processing data
}
After optimizing the code
async function fetchDataInParallel() {
const userDataPromise = fetchUserData();
const postsDataPromise = fetchPostsData(); // Assume we can fetch posts without waiting for userData
const [userData, postsData] = await Promise.all([userDataPromise, postsDataPromise]);
const commentsData = await fetchCommentsData(postsData.id);
// Processing data
}
Advanced Patterns: Async Generators and Async Iteration
Async generators and async iternations allow you to handle streams of data asynchronously. This helps you work with large datasets and streaming data.
Async Generators
Async Generators are functions that return values asynchronously, these functions return something called as an Asynciterator, which produces an stream of promises
lets see an example for Async Generator for Paginated Data.
async function* fetchPaginatedData(url: string) {
let currentPage = 0;
let hasNextPage = true;
while (hasNextPage) {
const response = await fetch(`${url}?page=${currentPage}`);
const data = await response.json();
yield data.items;
currentPage++;
hasNextPage = data.hasNextPage;
}
}
Async Iteration
With async iteration, you can use the loop for await ... of
to iterate over the async iterable objects that are produced by the async generator
example of consuming the Paginated data
async function consumeData() {
for await (const items of fetchPaginatedData('https://api.example.com/data')) {
for (const item of items) {
console.log(item); // Process each item
}
}
}
Metered TURN servers
API: TURN server management with powerful API. You can do things like Add/ Remove credentials via the API, Retrieve Per User / Credentials and User metrics via the API, Enable/ Disable credentials via the API, Retrive Usage data by date via the API.
Global Geo-Location targeting: Automatically directs traffic to the nearest servers, for lowest possible latency and highest quality performance. less than 50 ms latency anywhere around the world
Servers in 12 Regions of the world: Toronto, Miami, San Francisco, Amsterdam, London, Frankfurt, Bangalore, Singapore,Sydney, Seoul
Low Latency: less than 50 ms latency, anywhere across the world.
Cost-Effective: pay-as-you-go pricing with bandwidth and volume discounts available.
Easy Administration: Get usage logs, emails when accounts reach threshold limits, billing records and email and phone support.
Standards Compliant: Conforms to RFCs 5389, 5769, 5780, 5766, 6062, 6156, 5245, 5768, 6336, 6544, 5928 over UDP, TCP, TLS, and DTLS.
Multi‑Tenancy: Create multiple credentials and separate the usage by customer, or different apps. Get Usage logs, billing records and threshold alerts.
Enterprise Reliability: 99.999% Uptime with SLA.
Enterprise Scale: With no limit on concurrent traffic or total traffic. Metered TURN Servers provide Enterprise Scalability
5 GB/mo Free: Get 5 GB every month free TURN server usage with the Free Plan
Runs on port 80 and 443
Support TURNS + SSL to allow connections through deep packet inspection firewalls.
Support STUN
Supports both TCP and UDP
Need Chat API for your website or app
DeadSimpleChat is an Chat API provider
Add Scalable Chat to your app in minutes
10 Million Online Concurrent users
99.999% Uptime
Moderation features
1-1 Chat
Group Chat
Fully Customizable
Chat API and SDK
Pre-Built Chat