Async/Await in TypeScript: A step by step guide

Async/Await in TypeScript: A step by step guide

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 multiple await 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

  1. Use async/await for Most Async Code

  2. Promise Chains for Parallel Execution

  3. Handle errors with Try/Catch

  4. Avoid Mixing Callbacks, Promises and Async/Await

Example of how to convert the Promise based code to Async/Await

  1. 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

  1. 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

  1. Avoiding unnecessary await: Using await unnecessarily can lead to performance bottlenecks. Whenever possible consider using Promise.all

  2. Using Promise.all for parallel execution whenever feasible

  3. Cache 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

  4. 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

  1. 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.

  2. 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

  3. Servers in 12 Regions of the world: Toronto, Miami, San Francisco, Amsterdam, London, Frankfurt, Bangalore, Singapore,Sydney, Seoul

  4. Low Latency: less than 50 ms latency, anywhere across the world.

  5. Cost-Effective: pay-as-you-go pricing with bandwidth and volume discounts available.

  6. Easy Administration: Get usage logs, emails when accounts reach threshold limits, billing records and email and phone support.

  7. Standards Compliant: Conforms to RFCs 5389, 5769, 5780, 5766, 6062, 6156, 5245, 5768, 6336, 6544, 5928 over UDP, TCP, TLS, and DTLS.

  8. Multi‑Tenancy: Create multiple credentials and separate the usage by customer, or different apps. Get Usage logs, billing records and threshold alerts.

  9. Enterprise Reliability: 99.999% Uptime with SLA.

  10. Enterprise Scale: With no limit on concurrent traffic or total traffic. Metered TURN Servers provide Enterprise Scalability

  11. 5 GB/mo Free: Get 5 GB every month free TURN server usage with the Free Plan

  12. Runs on port 80 and 443

  13. Support TURNS + SSL to allow connections through deep packet inspection firewalls.

  14. Support STUN

  15. 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