A quick introduction to Promises and Async/Await

In this lesson, we are going to learn about ES6 Promises implementation in TypeScript and async/await syntax.

A quick introduction to Promises and Async/Await

Promises are a core part of modern JavaScript. In this lesson, we will focus on how TypeScript understands promises and how it types resolved values.

💡 If promises are new to you, first read a JavaScript-focused explanation of promises and async/await. This lesson assumes you already understand the basic Promise API.


A promise is an instance (object) of the Promise class (constructor). To create a promise, we use new Promise(*executor*) syntax and provide an executor function as an argument. This executor function provides a means to control the behavior of our promise resolution or rejection.

In TypeScript, we can provide the data type of the value returned when promise fulfills. Since the error returned by the promise can take any shape, the default data type of value returned when the promise is rejected is set to any by the TypeScript.

To annotate the resolution value type of the promise, we use a generic type declaration. Basically, you promise a type with Promise constructor in the form of new Promise<Type>() which indicates the resolved value type of the promise. But you can also use let p: Promise<Type> = new Promise() syntax to achieve the same.

💡 We discussed generics classes in detail in the Generics lesson.

// promise.ts
const getRandomInt = (): string => {
  return (Math.random() * 10).toFixed(0);
};

const findEven = new Promise<number>((resolve, reject) => {
  setTimeout(function (): void {
    const value = parseInt(getRandomInt());

    if (value % 2 === 0) {
      resolve(value);
    } else {
      reject("Odd number found!");
    }
  }, 1000);
});

findEven
  .then((value) => {
    console.log("Resolved:", value + 1);
  })
  .catch((error) => {
    console.log("Rejected:", error);
  })
  .finally(() => {
    console.log("Completed!");
  });
$ ts-node promise.ts
Resolved: 3
Completed!

$ ts-node promise.ts
Rejected: Odd number found!
Completed!

In this example, findEven is a promise which was created using the Promise constructor that resolves after 1 second. The resolved data type of this promise is number, so the TypeScript compiler won’t allow you to call resolve function with a value other than a value of type number number.

The default type of the promise’s rejection value is any, so calling reject function with any value is legal. This is the default behavior of TypeScript, and you can find the discussion thread here if you have your own opinions.

Since we have provided the number as the data type of successful promise resolution, the TypeScript compiler will provide the number type to the argument of value argument of the then callback method.

The callback provided in the then method is executed when the promise is resolved and the callback provided in catch method is executed when it rejects or some error while resolving the promise. The finally method registers a callback that executes when promise either resolves or rejects.

If the TypeScript compiler complains about the finally method, your project may not be loading the right library declarations. Make sure your tsconfig.json includes a modern enough lib setting for the Promise APIs you are using.

// tsconfig.json
{
  "files": ["./promise.ts"],
  "compilerOptions": {
    "lib": ["DOM", "ES2020"]
  }
}

In my tsconfig.json, I have loaded the ES2020 standard library. This provides support for all the JavaScript feature up until ES2020. If you want to know more about the tsconfig.json file or standard libraries, please read the Compilation lesson.

Promise Chaining

The then, catch and finally methods return a promise implicitly. Any value returned by these callback functions is wrapped with a promise and returned, including undefined. This implicit promise is resolved by default unless you are deliberately returning a new promise from these methods that could fail.

So you can append then, catch or finally methods to any of the previous then, catch or finally method. If an implicit promise is returned by one of these methods, then the resolved value type of this implicit promise is the type of the returned value. Let’s see a quick example.

// promise-chaining.ts
const getRandomInt = (): string => {
  return (Math.random() * 10).toFixed(0);
};

const findEven = new Promise<number>((resolve, reject) => {
  setTimeout(function (): void {
    const value = parseInt(getRandomInt());

    if (value % 2 === 0) {
      resolve(value);
    } else {
      reject("Odd number found!");
    }
  }, 1000);
});

findEven
  .then((value) => {
    console.log("Resolved-1:", value + 1);
    return `${value + 1}`;
  })
  .then((value) => {
    console.log("Resolved-2:", value + 1);
  })
  .catch((error) => {
    console.log("Rejected:", error);
  })
  .finally(() => {
    console.log("Completed!");
  });
$ ts-node promise-chaining.ts
Resolved-1: 5
Resolved-2: 51
Completed!

We have modified the previous example and added another then method to the first then method. Since the first then method returns a value of type string, the implicit promise returned by this method will be resolved with a value of the type string. So, the second then method will receive value argument of type string as you can see from the results.

Promise.resolve

The resolve static method of the Promise call returns a promise that is already resolved successfully with a value that you provide in the Promise.resolve(value) call. This is easier than creating a new instance of the Promise call and adding logic to resolve the promise immediately.

// promise-resolve.ts
const getRandomInt = (): string => {
  return (Math.random() * 10).toFixed(0);
};

const value: number = parseInt(getRandomInt());
const numPromise = Promise.resolve(value);

numPromise
  .then((value) => {
    console.log("Resolved:", value + 1);
  })
  .catch((error) => {
    console.log("Rejected:", error);
  });
$ ts-node promise-resolve.ts
Resolved: 2

As you can see from these results, the promise returned by the Promise.resolve(value) call always resolves immediately with a number value since the value argument has the type of number.

Promise.reject

Similar to Promise.resolve static method, the Promise.reject(error) method always returns a rejected promise. The value of the promise rejection is taken from the error_ argument and its type is any._

// promise-reject.ts
const message = "Oops! Something went wrong.";
const oopsPromise = Promise.reject(message);

oopsPromise
  .then((value) => {
    console.log("Resolved:", value);
  })
  .catch((error) => {
    console.log("Rejected:", error);
  });
$ ts-node promise-reject.ts
Rejected: Oops! Something went wrong.

The type of promise returned by the Promise.reject method is Promise<never> because this promise never resolves, so there won’t be any promise resolution value. Therefore the type of the value resolved by the promise is never as never signifies the value that never occurs.

Promise.all

In some scenarios, you are dealing with multiple promises. If you want to execute a callback function when all the promises are resolved successfully, use the Promise.[all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) static method.

var pAll = Promise.all([ p1, p2, ... ])
pAll.then( ( [ r1, r2, ... ] ) => {___});

The Promise.all method takes an array (iterable precisely) of promises and returns a new promise. The returned promise pAll is resolved when all promises p1, p2, ... are resolved successfully. This promise is resolved with an array value that contains the promise resolution values of p1, p2, ... in the order of their appearance.

// promise-all.ts
const getPromise = (
  value: number,
  delay: number,
  fail: boolean,
): Promise<number> => {
  return new Promise<number>((resolve, reject) => {
    setTimeout(() => (fail ? reject(value) : resolve(value)), delay);
  });
};

const allPromise = Promise.all<number>([
  getPromise(0, 0, false),
  getPromise(1, 2000, false),
  getPromise(2, 1000, false),
]);

console.time("settled-in");
allPromise
  .then((value) => {
    console.log("Resolved:", value);
  })
  .catch((error) => {
    console.log("Rejected:", error);
  })
  .finally(() => {
    console.timeEnd("settled-in");
  });
$ ts-node promise-all.ts
Resolved: [ 0, 1, 2 ]
settled-in: 2008.053ms

As you can see from this example, Promise.all is generic and tracks the resolved value type of each promise. That is useful because the final resolved value has a predictable type.

💡 Most static methods of the Promise class are generic, as the examples here show.

There is one caveat with Promise.all. It implements the fail-fast mechanism which means if any of the input promises p1, p2, ... are rejected, pAll is rejected. If won’t wait for other pending promises to resolve.

// promise-all-reject.ts
const getPromise = (
  value: number,
  delay: number,
  fail: boolean,
): Promise<number> => {
  return new Promise<number>((resolve, reject) => {
    setTimeout(() => (fail ? reject(value) : resolve(value)), delay);
  });
};

const allPromise = Promise.all<number>([
  getPromise(0, 0, false),
  getPromise(1, 2000, false),
  getPromise(2, 1000, true),
]);

console.time("settled-in");
allPromise
  .then((value) => {
    console.log("Resolved:", value);
  })
  .catch((error) => {
    console.log("Rejected:", error);
  })
  .finally(() => {
    console.timeEnd("settled-in");
  });
$ ts-node promise-all-reject.ts
Rejected: 2
settled-in: 1003.947ms

As you can see from this result, as the third promise was rejected just after 1 second, the allPromise was rejected immediately.

Promise.allSettled

The Promise.allSettled static method is similar to Promise.all but unlike Promise.all, it waits until all the promises are settled (which means until they resolved or rejected). So the promise returned by Promise.allSettled will never reject (but we have added catch block in this example anyway).

// promise-all-settled.ts
const getPromise = (
  value: number,
  delay: number,
  fail: boolean,
): Promise<number> => {
  return new Promise<number>((resolve, reject) => {
    setTimeout(() => (fail ? reject(value) : resolve(value)), delay);
  });
};

const allPromise = Promise.allSettled([
  getPromise(0, 0, false),
  getPromise(1, 2000, false),
  getPromise(2, 1000, true),
]);

console.time("settled-in");
allPromise
  .then((value) => {
    console.log("Resolved:", value);
  })
  .catch((error) => {
    console.log("Rejected:", error);
  })
  .finally(() => {
    console.timeEnd("settled-in");
  });
$ ts-node promise-all-settled.ts
Resolved: [
  { status: "fulfilled", value: 0 },
  { status: "fulfilled", value: 1 },
  { status: "rejected", reason: 2 }
]
settled-in: 2006.407ms

Working with Promise.allSettled can feel a little noisy at first. It returns a promise that resolves with an array of PromiseSettledResult<type> values after all promises settle. The type is derived from the input promise.

interface PromiseFulfilledResult<T> {
  status: "fulfilled";
  value: T; // promise resolved value
}

interface PromiseRejectedResult {
  status: "rejected";
  reason: any; // promise rejected value
}

type PromiseSettledResult<T> =
  | PromiseFulfilledResult<T>
  | PromiseRejectedResult;

These types are provided by TypeScript’s standard library. So when a promise resolves, allSettled method converts its value into PromiseFulfilledResult shape and when it fails, it converts it to PromiseRejectedResult shape. That’s why when allSettled is resolved, it’s an array of objects in which each object has a shape of PromiseFulfilledResult or PromiseRejectedResult interface.

Since PromiseFulfilledResult is a union of PromiseFulfilledResult and PromiseRejectedResult that has common status property of literal data type, we can use it as a discriminant in the switch/case guard.

💡 We have talked about switch/case type guard and discriminating unions in the Type System lesson.

// promise-all-settled.ts
console.time("settled-in");
allPromise
  .then((value) => {
    console.log("Resolved:", value);

    value.forEach((result) => {
      switch (result.status) {
        case "fulfilled": {
          console.log("success =>", result.value);
          break;
        }
        case "rejected": {
          console.log("error =>", result.reason);
          break;
        }
      }
    });
  })
  .catch((error) => {
    console.log("Rejected:", error);
  })
  .finally(() => {
    console.timeEnd("settled-in");
  });
$ ts-node promise-all-settled.ts
Resolved: [
  { status: "fulfilled", value: 0 },
  { status: "fulfilled", value: 1 },
  { status: "rejected", reason: 2 }
]
success => 0
success => 1
error => 2
settled-in: 2009.868ms

Promise.race

The Promise.race takes an array (iterable precisely) of promises and returns a new promise that resolves or rejects as soon as one of the input promises resolves or rejects. In other words, the promise returned by Promise.race is settled with the result of one of the input promises which settles quickly.

// promise-race.ts
const getPromise = (
  value: number,
  delay: number,
  fail: boolean,
): Promise<number> => {
  return new Promise<number>((resolve, reject) => {
    setTimeout(() => (fail ? reject(value) : resolve(value)), delay);
  });
};

const fastestPromise = Promise.race<number>([
  getPromise(0, 500, false),
  getPromise(1, 2000, false),
  getPromise(2, 1000, true),
]);

console.time("settled-in");
fastestPromise
  .then((value) => {
    console.log("Resolved:", value);
  })
  .catch((error) => {
    console.log("Rejected:", error);
  })
  .finally(() => {
    console.timeEnd("settled-in");
  });
$ ts-node promise-race.ts
Resolved: 0
settled-in: 506.430ms

Unlike Promise.all or Promise.allSettled, Promise.race returns the value from the first settled promise. In this case, the returned type is Promise<number>, and the then callback runs after the first promise settles.

💡 The new Promise.any() method has reached Stage 4 of the ECMAScript proposal track. Promise.any is much like Promise.race but it waits until the first promise is successful resolves. This method throw an AggregateError exception if all the promises are rejected.

Async/Await keywords

Writing promises the normal way seems a bit difficult to manage. There is a lot of unnecessary boilerplate code involved. Normally, you have a function that processes some arguments and returns a promise that resolves or rejects with some meaningful data. Adding a promise creation mechanism using Promise constructor seems just icky at times.

JavaScript provides the async keyword to turn a normal function into a function that returns a promise implicitly. Any value returned by this async function is wrapped in a resolved promise. If the function throws an error, the returned promise is rejected.

The await keyword is used inside an async function to wait on a promise. If a promise has the await keyword before it, the function execution won’t proceed further until that promise is resolved.

// async-await.ts
const getRandomInt: () => Promise<number> = async () => {
  return parseInt((Math.random() * 10).toFixed(0));
};

const isEven = async (answer: boolean) => {
  const value = await getRandomInt();
  const isEven = value % 2 === 0;

  return isEven === answer;
};

isEven(true)
  .then((value) => {
    console.log(value === true ? "lucky :)" : "unlucky :(");
  })
  .catch((error) => {
    console.log("Rejected:", error);
  })
  .finally(() => {
    console.log("Completed!");
  });
$ ts-node async-await.ts
lucky :)
Completed!

In this example, getRandomInt is an async function because it uses the async keyword. Since it resolves with a number, TypeScript understands its return type as a promise of number.

const getRandomInt: () => Promise<number>;

However, the explicit type is not always necessary. TypeScript understands the async keyword and infers the function return type from the returned value.

const isEven: (answer: boolean) => Promise<boolean>;

We have used await keyword inside the isEven function expression to wait for the resolution of the promise returned by the getRandomInt function. This means isEven function execution is basically going to halt until this promise is resolved, including the value assignment expression.

Since TypeScript knows about the type of the promise returned by the getRandomInt function, the value constant has the number type. The isEven async function returns a promise that resolved with a boolean value.

You might wonder, how the promise returned by a async function is going to reject? Well, I have explained this in my other article on Promises but in a nutshell, the promise returned by an async function rejects if an error is thrown inside the async function.

In this example, we modify the previous code and add a check inside isEven. If value is 0, the function throws a regular JavaScript error.

In this scenario, there is a chance that the promise returned by the isEven function may get rejected. This will be caught by the catch method and it will contain the error that was thrown in the async function.

We have provided the Error type for the error argument of the callback function of the catch method to assert the default any type to Error type since we know the rejection value type beforehand.

Since an async function uses throw keyword to throw an error in order to reject the promise, it raises another question. What will happen if another async function is awaiting on the promise if that promise gets rejected?

The short answer is that errors bubble through async functions. If an awaited promise rejects, JavaScript throws inside the current async function and the code after that await does not run.

To check if the awaited promise was rejected, we use try/catch. If we await a promise inside a try block, we will know if the promise was rejected in the catch block. The argument to catch block is the rejection value.

💡 FYI, you can also await a promise returned by the Promise constructor. There won’t be any difference whatsoever. If it rejects, it will result in an error.

// async-await-reject.ts
const getRandomInt: () => Promise<number> = async () => {
  return parseInt((Math.random() * 10).toFixed(0));
};

const isEven = async (answer: boolean) => {
  const value = await getRandomInt();

  if (value === 0) {
    throw new Error("Can't work with 0 :/");
  }

  const isEven = value % 2 === 0;
  return isEven === answer;
};

isEven(true)
  .then((value) => {
    console.log(value === true ? "lucky :)" : "unlucky :(");
  })
  .catch((error: Error) => {
    console.log("Rejected:", error.message);
  })
  .finally(() => {
    console.log("Completed!");
  });
$ ts-node async-await-reject.ts
Rejected: Can't work with 0 :/
Completed!

In this example, the promise returned by the getRandomInt can get rejected. So inside the isEven async function, we have wrapped the awaiting logic inside the try block. If the code inside try block throws an error, the catch block will be executed with that error.

In this example, the isEven async function always returns a promise that never rejects. If you want to reject this promise, you need to throw an error manually using throw keyword within the catch block.

#typescript #promises