Promises

After seeing how the use of callbacks can lead to some maintainability problems, we are now going to learn about promises and how they can be used to write better asynchronous code. The core idea behind promises is that a promise represents the result of an asynchronous operation. A promise must be in one of the following three states:

  • Pending: The initial state of a promise.
  • Fulfilled/resolved: The state of a promise representing a successful operation. The terms "fulfilled" and "resolved" are both commonly used to refer to this state.
  • Rejected: The state of a promise representing a failed operation.

Once a promise is fulfilled or rejected, its state can never change again. Let's look at the basic syntax of a promise:

function foo() { 
    return new Promise<string>((fulfill, reject) => { 
        try { 
            // do something 
            fulfill("SomeValue"); 
        } catch (e) { 
            reject(e); 
        } 
    }); 
} 
 
foo().then((value) => { 
    console.log(value); 
}).catch((e) => { 
    console.log(e); 
}); 
A try...catch statement is used here to showcase how we can explicitly fulfill or reject a promise. The try...catch statement is not needed for a Promise function because when an error is thrown in a promise callback, the promise will automatically be rejected.

The preceding code snippet declares a function named foo that returns a promise. The promise contains a method named then, which accepts a function to be invoked when the promise is fulfilled. Promises also provide a method named catch, which is invoked when a promise is rejected.

Promises will not be recognized by the TypeScript compiler if we are targeting ES5, because the promises API is part of ES6. We can solve this by enabling the es2015.promise type using the lib option in the tsconfig .json file. Note that enabling this option will disable some types that are included by default and therefore break some of the examples. You will be able to solve the problems by including the dom and es5 types, as well as by using the lib option in the tsconfig .json file:
"lib": [
    "es2015.promise",
    "dom",
    "es5",
    "es2015.generator", // new
    "es2015.iterable" // new
] 
We will learn more about the lib setting in Chapter 9 , Automating Your Development Workflow .

We are now going to rewrite the doSomethingAsync, doSomethingElseAsync, and doSomethingMoreAsync functions that we wrote during the callback hell example, using promises instead of callbacks:

function doSomethingAsync(arr: number[]) { 
    return new Promise<number[]>((resolve, reject) => { 
        setTimeout(() => { 
            try { 
                let n = Math.ceil(Math.random() * 100 + 1); 
                if (n < 25) { 
                    throw new Error("n is < 25"); 
                } 
                resolve([...arr, n]); 
            } catch (e) { 
                reject(e); 
            } 
        }, 1000); 
    }); 
} 
 
function doSomethingElseAsync(arr: number[]) { 
    return new Promise<number[]>((resolve, reject) => { 
        setTimeout(() => { 
            try { 
                let n = Math.ceil(Math.random() * 100 + 1); 
                if (n < 25) { 
                    throw new Error("n is < 25"); 
                } 
                resolve([...arr, n]); 
            } catch (e) { 
                reject(e); 
            } 
        }, 1000); 
    }); 
} 
 
function doSomethingMoreAsync(arr: number[]) { 
    return new Promise<number[]>((resolve, reject) => { 
        setTimeout(() => { 
            try { 
                let n = Math.ceil(Math.random() * 100 + 1); 
                if (n < 25) { 
                    throw new Error("n is < 25"); 
                } 
                resolve([...arr, n]); 
            } catch (e) { 
                reject(e); 
            } 
        }, 1000); 
    }); 
} 
 

We can chain the promises that are returned by each of the preceding functions using the promises API:

doSomethingAsync([]).then((arr1) => { 
    doSomethingElseAsync(arr1).then((arr2) => { 
        doSomethingMoreAsync(arr2).then((arr3) => { 
            console.log( 
                ` 
                doSomethingAsync: ${arr3[0]} 
                doSomethingElseAsync: ${arr3[1]} 
                doSomethingMoreAsync: ${arr3[2]} 
                ` 
            ); 
        }); 
    }); 
}).catch((e) => console.log(e)); 

The preceding code snippet is a little bit better than the one used in the callback example because we only needed to declare one exception handler instead of three exception handlers. This is possible because errors are propagated through the chain of promises.

The preceding example has introduced some improvements. However, the promises API allows us to chain promises in a much less verbose way:

doSomethingAsync([]) 
    .then(doSomethingElseAsync) 
    .then(doSomethingMoreAsync) 
    .then((arr3) => { 
        console.log( 
            ` 
            doSomethingAsync: ${arr3[0]} 
            doSomethingElseAsync: ${arr3[1]} 
            doSomethingMoreAsync: ${arr3[2]} 
            ` 
        ); 
    }).catch((e) => console.log(e)); 

The preceding code is much easier to read and follow than the one used during the callback examples, but this is not the only reason to prefer promises over callbacks. Using promises also gives us better control over the execution flow of operations. Let's look at a couple of examples.

The promises API includes a method named all, which allows us to execute a series of promises in parallel and get all the results of each of the promises at once:

Promise.all([ 
    new Promise<number>((resolve) => { 
        setTimeout(() => resolve(1), 1000); 
    }), 
    new Promise<number>((resolve) => { 
        setTimeout(() => resolve(2), 1000); 
    }), 
    new Promise<number>((resolve) => { 
        setTimeout(() => resolve(3), 1000); 
    }) 
]).then((values) => { 
    console.log(values); // [ 1 ,2, 3] 
}); 

The promises API also includes a method named race, which allows us to execute a series of promises in parallel and get the result of the first promise resolved:

Promise.race([ 
    new Promise<number>((resolve) => { 
        setTimeout(() => resolve(1), 3000); 
    }), 
    new Promise<number>((resolve) => { 
        setTimeout(() => resolve(2), 2000); 
    }), 
    new Promise<number>((resolve) => { 
        setTimeout(() => resolve(3), 1000); 
    }) 
]).then((fastest) => { 
    console.log(fastest); // 3 
});

We can use many different types of asynchronous flow control when working with promises:

  • Concurrent: The tasks are executed in parallel (like in the Promise.all example)
  • Race: The tasks are executed in parallel, and only the result of the fastest promise is returned
  • Series: A group of tasks is executed in sequence, but the preceding tasks do not pass arguments to the next task
  • Waterfall: A group of tasks is executed in sequence, and each task passes arguments to the next task (like in the example that preceded the Promise.all and Promise.race examples)
  • Composite: This is any combination of the preceding concurrent, series, and waterfall approaches