This pattern enables sequential processing of promises (tasks) as well as capturing and collecting the return values.

const randomString = len =>
    [...Array(len || 10)]

const wait = async (ms, text) => {
    text = text || randomString()
    return new Promise((resolve, reject) =>
        setTimeout(() => resolve(text), ms))

const processTasks = arr => {
        arr.reduce((promise_chain, current, i) => {
            const current_task = wait(current)
            console.log('-- promise_chain', promise_chain)
            console.log('-- running task', (i+1))
            return promise_chain
                .then(chain_results => {
                    console.log('- chain_results', chain_results)
                    return current_task
                        .then(current_result => {
                            return [
        }, Promise.resolve([]))
            .then(results => {

;(async () => {
    let arr = [ 2500, 3725, 1275, 3000 ]
    await processTasks(arr)

The function which is passed in itself takes at least two parameters: the previous and the current value, in this case, the promise chain and a value which will be used to create a promise task.

The initial value (last parameter) of the reducer function is a promise which immediately resolves to an empty array.

The reducer function operates by executing the promise chain and then the current task AS WELL AS then collecting up the return values which are then available at the end.

In the following JDoodle execute the code to see how this works in practice.