Promises are one of the newest features introduced in the JavaScript language. Since I have another article just on Promises and Async/Await syntax in JavaScript, I am just going to focus on how Promises are implemented in TypeScript.
💡 Please follow this above article to know more about how Promises work under the hood in the JavaScript engine. Having the knowledge Promises absolutely necessary to make sense of whatever we are going to talk about in this lesson.
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 have discussed generics classes in detail in the Generics lesson.
In the above 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
, hence 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
, hence 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, that means your TypeScript compiler doesn’t import type definitions for the finally
method. This method was introduced in ES2016, hence it’s quite new. Other features of the Promise API used in this lesson are pretty new, hence make sure your tsconfig.json
file has all the new libraries loaded.
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 (coming soon).
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.
Hence 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.
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
. Hence, 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.
As you can see from the above 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
._
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.
As you can see from the above example, Promise.all
method is generic and it takes the type the value resolved by each promise provided to it. Providing a generic type is quite helpful when we are using the resolved value of this collective promise as you can see from the above result.
💡 Most of the static methods of the
Promise
class are generic as demonstrated in the below examples.
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.
As you can see from the above result, as the 3rd 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). Hence the promise returned by Promise.allSettled
will never reject (but we have added catch
block in the below example anyway).
Working with promise.allSettled
can be a little overwhelming as you can from the above program and its result. First of all, allSettle
method returns a promise that resolves with an array of PromiseSettledResult<type>
values when all promises are settled. The type
is derived from the type of the input promise. The PromiseSettledResult
type looks like below.
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.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.
Unlike Promise.all
or Promise.allSettled
, this method only returns a single value of the first settled promise, hence the type of the returned promise is Promise<number>
in the above case. Since the 1st promise settled first among others, the then
callback of the fastestPromise
gets called after 500ms with the value of the resolved promise.
💡 The new
Promise.any()
method has reached Stage 4 of the ECMAScript proposal track.Promise.any
is much likePromise.race
but it waits until the first promise is successful resolves. This method throw anAggregateError
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 provide async
keyword to turn a normal function into a function that returns a promise implicitly. Any value returned by this async function will be converted to an implicit promise that resolves with this value. If an error is thrown in this function, the returned promise is rejected with the error message.
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.
In the above example, the getRandomInt
is an async function since it has async
keyword before the function expression. Since this function returns a promise which resolves with a number
value, we have provided the correct type for the getRandomInt
value (shown below).
const getRandomInt: () => Promise<number>;
However, this is not necessary. TypeScript understands the async
keyword, as well as looks at the return value type of the function to provide an implicit type for the function. Hence, the isEven
constant has the below type.
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.
xIn the above example, we have done some modifications to the previous example. We have added a check inside isEven
function that checks if the value
is 0
then throws a generic 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, errors are bubbled up in the async functions. This means if the promise we are awaiting (using await
keyword) rejects, JavaScript will throw an error in the current async function and nothing below that await
keyword will execute. This goes on if the current async function is also being used inside another async function.
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.
In the above example, the promise returned by the getRandomInt
can get rejected. Hence 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.