Mastering Async Programming in TypeScript: Promises, Async/Await, and Callbacks

Mastering Async Programming in TypeScript: Promises, Async/Await, and Callbacks

#typescript
Isaiah Clifford Opoku
Isaiah Clifford Opoku
Dec 1, 2025·15 min read
15 min
reading time

Async programming is a programming paradigm that allows you to write code that runs asynchronously. In contrast to synchronous programming, which executes code sequentially, async programming allows code to run in the background while the rest of the program continues to execute. This is particularly useful for tasks that may take a long time to complete, such as fetching data from a remote API.

In JavaScript, async programming is essential for creating responsive and efficient applications. TypeScript, a superset of JavaScript, makes it even easier to work with async programming. There are several approaches to async programming in TypeScript, including promises, async/await, and callbacks. we will cover each of these approaches in detail, so that you can choose the best one for your use case.

Why is Async Programming Important?

  • Async programming is essential for building modern web applications that are responsive and efficient. By allowing tasks to run in the background while the rest of the program continues to execute, async programming ensures that the user interface remains responsive to user input. Additionally, async programming can improve the overall performance of the application by allowing multiple tasks to run concurrently.

There are many examples of how async programming can be used in real-world applications, such as making HTTP requests, accessing user cameras and microphones, and handling user input events. Even if you don't need to implement your own asynchronous functions very often, it's important to understand how to use them correctly to ensure that your application is reliable and performs well.

How TypeScript Makes Async Programming Easier

TypeScript provides several features that make it easier to work with async programming, including type safety, type inference, type checking, and type annotations. With type safety, you can be sure that your code will work as expected, even when working with asynchronous functions. For example, TypeScript can catch errors related to null and undefined values at compile time, which can save you time and effort in debugging. TypeScript's type inference and checking also reduce the amount of boilerplate code you need to write, which can make your code more concise and easier to read. Additionally, TypeScript's type annotations provide clarity and documentation for your code, which can be especially helpful when working with asynchronous functions that can be difficult to reason about.

Promises in TypeScript

  • Promises are a powerful tool for working with asynchronous operations in TypeScript. For example, you might use a Promise to fetch data from an external API, or to execute a time-consuming task in the background while your main thread continues to run. To use a Promise, you create a new instance of the Promise class and pass it a function that performs the asynchronous operation. This function should call the resolve method with the eventual result when the operation succeeds, or the reject method with an error when the operation fails. Once the Promise is created, you can attach callbacks to it using the then method. These callbacks will be called when the Promise is fulfilled, with the resolved value passed as a parameter. If the Promise is rejected, you can attach an error handler using the catch method, which will be called with the reason for the rejection.

Using Promises can offer several benefits over traditional callback-based approaches. For example, Promises can help prevent "callback hell", a common problem in asynchronous code where nested callbacks can become difficult to read and maintain. Promises also make it easier to handle errors in asynchronous code, as you can use the catch method to handle errors that occur anywhere in the Promise chain. Finally, Promises can simplify your code by providing a consistent, composable way to handle asynchronous operations, regardless of their underlying implementation.

Creating Promises

The promise syntax

TStypescript
1const myPromise = new Promise((resolve, reject) => { 2 // Do some asynchronous operation 3 // If the operation is successful, call resolve with the result 4 // If the operation fails, call reject with an error object 5}); 6 7myPromise 8 .then(result => { 9 // Handle the successful result 10 }) 11 .catch(error => { 12 // Handle the error 13 });
TStypescript
1// Example 1 on how to create a promise 2 3function myAsyncFunction(): Promise<string> { 4 return new Promise<string>((resolve, reject) => { 5 // Some asynchronous operation 6 setTimeout(() => { 7 // Successful operation resolves promiseCheck out my latest blog post on mastering async programming in TypeScript! Learn how to work with Promises, Async/Await, and Callbacks to write efficient and scalable code. Get ready to take your TypeScript skills to the next level! 8 const success = true; 9 10 if (success) { 11 // Resolve the promise with the operation result if the operation was successful 12 resolve( 13 `The result is success and your operation result is ${operationResult}` 14 ); 15 } else { 16 const rejectCode: number = 404; 17 const rejectMessage: string = `The result is failed and your operation result is ${rejectCode}`; 18 // Reject the promise with the operation result if the operation failed 19 reject(new Error(rejectMessage)); 20 } 21 }, 2000); 22 }); 23} 24 25// Use the promise 26myAsyncFunction() 27 .then(result => { 28 console.log(result); // output : The result is success and your operation result is 4 29 }) 30 .catch(error => { 31 console.error(error); // output : The result is failed and your operation result is 404 32 });

In above example, we have a function called myAsyncFunction() that returns a promise. We use the Promise constructor to create the promise, which takes a callback function with a resolve and reject argument. If the asynchronous operation is successful, we call the resolve function, and if it fails, we call the reject function.

The promise object that's returned by the constructor has a then() method which takes a success and failure callback function. If the promise resolves successfully, the success callback function is called with the result. If the promise rejects, the failure callback function is called with an error message.

Additionally, the promise object also has a catch() method which is used to handle errors that occur during the promise chain. The catch() method takes a callback function which is called if any error occurs in the promise chain.

Now let move how to perform chaining promises in typescript

Chaining Promises

  • Chaining promises is a way to perform multiple asynchronous operations in a sequence or parallel. This is useful when you need to perform multiple async operations one after the other, or simultaneously. For example, you may need to fetch data asynchronously and then process it asynchronously.

let see example on how to chain promises.

TStypescript
1// Example On how chaining promises works 2// First promise 3const promise1 = new Promise((resolve, reject) => { 4 const functionOne: string = 'This is the first promise function'; 5 setTimeout(() => { 6 resolve(functionOne); 7 }, 1000); 8}); 9 10// Second promise 11const promise2 = (data: number) => { 12 const functionTwo: string = 'This is the second second promise function'; 13 return new Promise((resolve, reject) => { 14 setTimeout(() => { 15 resolve(` ${data} '+' ${functionTwo} `); 16 }, 1000); 17 }); 18}; 19 20// Chaining first and second promises together 21promise1 22 .then(promise2) 23 .then(result => { 24 console.log(result); // output : This is the first promise function + This is the second second promise function 25 }) 26 .catch(error => { 27 console.error(error); 28 });

In above example, we have two promises:promise1 and promise2. promise1 resolves after 1 second with the string "This is the first promise function". promise2 takes a number as input and returns a promise that resolves after 1 second with a string that concatenates the input number and the string "This is the second promise function".

We then chain the two promises together using the then method. The output of promise1 is passed as input to promise2. Finally, we use the then method again to log the output of promise2 to the console. If either promise1 or promise2 rejects, the error will be caught by the catch method.

Congratulations! You have learned how to create and chain promises in TypeScript. You can now use promises to perform asynchronous operations in TypeScript. so now let Async / Await in TypeScript let see how it works

Async / Await

  • Async/await is a syntax that was introduced in ES2017 to make working with Promises easier. It provides a way to write asynchronous code that looks and feels like synchronous code. In TypeScript, you can define an asynchronous function using the async keyword. This tells the compiler that the function is asynchronous and will return a Promise.

now let see how to use async / await in typescript

Async / Await Syntax

TStypescript
1// Async / Await Syntax in TypeScript 2async function functionName(): Promise<ReturnType> { 3 try { 4 const result = await promise; 5 // code to execute after promise resolves 6 return result; 7 } catch (error) { 8 // code to execute if promise rejects 9 throw error; 10 } 11}

In above example above, functionName is an async function that returns a Promise of ReturnType. The await keyword is used to wait for the promise to resolve before continuing with the next line of code.

The try/catch block is used to handle any errors that occur while executing the code inside the async function. If an error occurs, it will be caught by the catch block, where you can handle it appropriately.

Using Arrow Functions with Async / Await

You can also use arrow functions with async/await syntax in TypeScript:

TStypescript
1const functionName = async (): Promise<ReturnType> => { 2 try { 3 const result = await promise; 4 // code to execute after promise resolves 5 return result; 6 } catch (error) { 7 // code to execute if promise rejects 8 throw error; 9 } 10};

In above example, the functionName is defined as an arrow function that returns a Promise of ReturnType. The async keyword is used to indicate that this is an asynchronous function, and the await keyword is used to wait for the promise to resolve before continuing with the next line of code.

Async / Await with api call

Now let more then syntax let fetch some api using async / await

TStypescript
1interface User { 2 id: number; 3 name: string; 4 email: string; 5} 6 7const fetchApi = async (): Promise<void> => { 8 try { 9 const response = await fetch('https://jsonplaceholder.typicode.com/users'); 10 11 if (!response.ok) { 12 throw new Error( 13 `Failed to fetch users (HTTP status code: ${response.status})` 14 ); 15 } 16 17 const data: User[] = await response.json(); 18 console.log(data); 19 } catch (error) { 20 console.error(error); 21 throw error; 22 } 23}; 24 25fetchApi();

What we doing is that just fetch api form jsonplaceholder and then convert it to json and then log it to the console. This is real world example on how to use async / await in typescript.

Async/Await with axios api call

TStypescript
1// Example 2 on how to use async / await in typescript 2 3const fetchApi = async (): Promise<void> => { 4 try { 5 const response = await axios.get( 6 'https://jsonplaceholder.typicode.com/users' 7 ); 8 const data = await response.data; 9 console.log(data); 10 } catch (error) { 11 console.error(error); 12 } 13}; 14 15fetchApi();

In above example, we define the fetchApi() function using async/await and the axios.get() method to make an HTTP GET request to the specified URL. We use await to wait for the response to be returned, and then extract the data from the response using the data property of the response object. Finally, we log the data to the console using console.log(). Any errors that occur are caught and logged to the console using console.error().

Note ; Before you can try the above code you need to install axios using npm or yarn

SHbash
1 2npm install axios 3
SHbash
1 2yarn add axios 3

If you don't have any idea on what axios is you can read more about it here axios

You can notice that we just used try and catch block to handle error. Try catch block is a way of handling error in typescript. So anytime you want to make and api calls like what we just did make sure you use try and catch block to handle error.

Now let more advance of using try and catch block in typescript

TStypescript
1// Example 3 on how to use async / await in typescript 2 3interface User { 4 id: number; 5 name: string; 6 email: string; 7 profilePicture: string; 8} 9 10const fetchEmployees = async (): Promise<Array<User> | string> => { 11 const api = 'http://dummy.retapiexample.com/api/v1/employees'; 12 try { 13 const response = await fetch(api); 14 const { data } = await response.json(); 15 return data; 16 } catch (error) { 17 if (error) { 18 return error.message; 19 } 20 } 21}; 22 23fetchEmployees().then(data => { 24 console.log(data); 25});

in above example, we define an interface User that describes the shape of the data we expect to receive from the API. We then define the fetchEmployees() function using async/await and the fetch() method to make an HTTP GET request to the specified API endpoint.

We use a try/catch block to handle any errors that might occur during the API request. If the request is successful, we extract the data property from the response using await and return it. If an error occurs, we check if there is an error message, and if so, return it as a string.

Finally, we call the fetchEmployees() function and use .then() to log the returned data to the console. This example demonstrates how to use async/await with try/catch blocks to handle errors in a more advanced scenario, where we need to extract data from a response object and return a custom error message.

Async / Await with Promise.all

  • Promise.all() is a method that takes an array of promises as an input (an iterable), and returns a single Promise as an output. This Promise will resolve when all of the input's promises have resolved, or if the input iterable contains no promises. It rejects immediately upon any of the input promises rejecting or non-promises throwing an error, and will reject with this first rejection message / error.
TStypescript
1// Example of using async / await with Promise.all 2interface User { 3 id: number; 4 name: string; 5 email: string; 6 profilePicture: string; 7} 8 9interface Post { 10 id: number; 11 title: string; 12 body: string; 13} 14 15interface Comment { 16 id: number; 17 postId: number; 18 name: string; 19 email: string; 20 body: string; 21} 22 23const fetchApi = async <T>(url: string): Promise<T> => { 24 try { 25 const response = await fetch(url); 26 if (response.ok) { 27 const data = await response.json(); 28 return data; 29 } else { 30 throw new Error(`Network response was not ok for ${url}`); 31 } 32 } catch (error) { 33 console.error(error); 34 throw new Error(`Error fetching data from ${url}`); 35 } 36}; 37 38const fetchAllApis = async (): Promise<[User[], Post[], Comment[]]> => { 39 try { 40 const [users, posts, comments] = await Promise.all([ 41 fetchApi<User[]>('https://jsonplaceholder.typicode.com/users'), 42 fetchApi<Post[]>('https://jsonplaceholder.typicode.com/posts'), 43 fetchApi<Comment[]>('https://jsonplaceholder.typicode.com/comments'), 44 ]); 45 return [users, posts, comments]; 46 } catch (error) { 47 console.error(error); 48 throw new Error('Error fetching data from one or more APIs'); 49 } 50}; 51 52fetchAllApis() 53 .then(([users, posts, comments]) => { 54 console.log('Users: ', users); 55 console.log('Posts: ', posts); 56 console.log('Comments: ', comments); 57 }) 58 .catch(error => console.error(error));

In the above code we are using Promise.all to fetch multiple api at once. So if you have multiple api to fetch you can use Promise.all to fetch them at once. So you can see that we are using map to loop through the array of api and then we pass it to Promise.all to fetch them at once.

let see how to use Promise.all with axios

TStypescript
1// Example of using async / await with axios and Promise.all 2 3const fetchApi = async () => { 4 try { 5 const urls = [ 6 'https://jsonplaceholder.typicode.com/users', 7 'https://jsonplaceholder.typicode.com/posts', 8 ]; 9 const responses = await Promise.all(urls.map(url => axios.get(url))); 10 const data = await Promise.all(responses.map(response => response.data)); 11 console.log(data); 12 } catch (error) { 13 console.error(error); 14 } 15}; 16 17fetchApi();

In above example, we're using Promise.all to fetch data from two different URLs simultaneously. We first make an array of the URLs, then use map to create an array of Promises from the axios.get calls. We then pass that array to Promise.all, which returns an array of responses. Finally, we use map again to extract the data from each response, and log it to the console.

Callbacks

  • A callback is a function that is passed as an argument to another function. The callback function is called (or executed) inside the other function. Callbacks are used to make sure that a function is not going to run before a task is completed but will run right after the task has completed. It helps us develop asynchronous JavaScript code and keeps us safe from problems and errors.
TStypescript
1// Example of using callbacks in typescript 2 3const add = (a: number, b: number, callback: (result: number) => void) => { 4 const result = a + b; 5 callback(result); 6}; 7 8add(10, 20, result => { 9 console.log(result); 10});

let see another example of using callbacks in typescript

TStypescript
1// Example of using a callback function in TypeScript 2 3type User = { 4 name: string; 5 email: string; 6}; 7 8const fetchUserData = ( 9 id: number, 10 callback: (error: Error | null, user: User | null) => void 11) => { 12 const api = `https://jsonplaceholder.typicode.com/users/${id}`; 13 fetch(api) 14 .then(response => { 15 if (response.ok) { 16 return response.json(); 17 } else { 18 throw new Error('Network response was not ok.'); 19 } 20 }) 21 .then(data => { 22 const user: User = { 23 name: data.name, 24 email: data.email, 25 }; 26 callback(null, user); 27 }) 28 .catch(error => { 29 callback(error, null); 30 }); 31}; 32 33// Usage of fetchUserData with a callback function 34fetchUserData(1, (error, user) => { 35 if (error) { 36 console.error(error); 37 } else { 38 console.log(user); 39 } 40});

In above example, we have a function fetchUserData that takes an id parameter and a callback parameter. The callback parameter is a function that takes two parameters: an error and a user. The function fetchUserData fetches user data from a JSONPlaceholder API endpoint based on the id, and if the fetch is successful, it constructs a User object and passes it to the callback function with a null error. If there's an error during the fetch, it passes the error to the callback function with a null user.

To use the fetchUserData function with a callback, we pass in an id and a callback function as arguments. The callback function checks for errors and logs the user data if there are no errors.

THANK YOU FOR READING MY ARTICLE. I HOPE YOU ENJOYED IT. IF YOU HAVE ANY QUESTIONS, FEEL FREE TO ASK IN THE COMMENT SECTION BELOW.

Conclusion

In this article, we have learned about the different ways to handle asynchronous code in TypeScript. We have learned about callbacks, promises, async/await, and how to use them in TypeScript. We have also learned about the this concept.

Connect with me on social media Twitter Github Linkedin

Tags

#typescript
Back to all posts

Found this helpful?

I'm posting .NET and software engineering content on multiple Social Media platforms.

If you enjoyed this article and want to see more content like this, consider subscribing to my newsletter or following me on social media for the latest updates.