-1

I have a typescript program which calls various commands in an external service. It uses Promises to handle successful completion or error conditions. There is a sequence of commands, for example:

server.doTask1()
  .then(server.doTask2())
  .then(server.doTask3())
  .catch( ... handle the error )

It is important that each task complete successfully before continuing to the next, so if any fail, it falls straight through to the catch() without attempting more tasks.

The problem comes because one of the tasks itself returns asynchronously - the promise fulfils as soon as the task has been successfully kicked off. There is another external command which I can call to check on progress. My attempt looks like this:

server.doTask1()
  .then(server.doTask2())
  .then(server.doTask3())
  .then(server.doLongRunningTask())

and...

doLongRunningTask() {
    return new Promise((resolve, reject) => {
        this.runCommand('external command')
        .then((result) => resolve(this.waitForCommand(result.Id)))
        .catch((err) => reject(err));
    });
}

private waitForCommand(commandId : string) : Promise<any> {

    while (true) {

        this.runCommand(`external command status report ${commandId}`)
            .then((result) => {
                if (result.Status==='IN_PROGRESS') {
                    console.log(`Status is ${result.Status}, waiting...`);
                    this.wait(60);
                } else {
                    console.log(`Status complete`);
                    return Promise.resolve();
                }
            })
            .catch((error) => {
                return Promise.reject(error);
            })

    } 
}

public wait(seconds : number) {
    var now = new Date().getTime();
    while(new Date().getTime() < now + (seconds*1000)){ /* Do nothing */ }
}

The intention is for waitForCommand to check the status once every minute and fulfil the promise when the long-running tasks reports that it is complete. But what actually happens is that it does not wait a minute between each check, and many lines 'Status is IN_PROGRESS, waiting...' appearing, and the process becoming unresponsive.

For the wait, i tried await new Promise(res => setTimeout(res, 60000)); but the Typescript compiler complains 'await expressions are only allowed within async functions and at the top levels of modules'

How can I achieve my desired result?

NickJ
  • 9,380
  • 9
  • 51
  • 74
  • *"It is important that each task complete successfully before continuing to the next"* That isn't what the code above it does, unless `doTask2` and `doTask3` **don't** do their tasks, but return functions that will do them when called later. – T.J. Crowder Aug 21 '21 at 15:54
  • 1
    *"The problem comes because one of the tasks itself returns asynchronously - the promise fulfils as soon as the task has been successfully kicked off."* That seems likea very odd design. The point of using promises is not setting them until an asynchronous tasks **completes**. – T.J. Crowder Aug 21 '21 at 15:54
  • 1
    I'm afraid it's not clear what you're really working with, which makes it basically impossible to give you any specific help. :-| – T.J. Crowder Aug 21 '21 at 15:56
  • 1
    Does this question help?https://stackoverflow.com/questions/68594022/how-to-keep-calling-a-api-once-a-specific-field-of-response-is-ready – ikhvjs Aug 21 '21 at 15:57
  • @T.J.Crowder "That isn't what the code above it does" - I think it does - each of those methods return a promise which is fulfilled when the task completes. – NickJ Aug 21 '21 at 17:31
  • What does `runCommand` do? Why does it return a promise if it settles that promise before the work is complete? I suspect we can do away with `waitForCommand` entirely, but the details will tell us whether that's true. – T.J. Crowder Aug 22 '21 at 07:47

2 Answers2

0

In a comment I said:

"It is important that each task complete successfully before continuing to the next" That isn't what the code above it does, unless doTask2 and doTask3 don't do their tasks, but return functions that will do them when called later.

You replied:

"That isn't what the code above it does" - I think it does - each of those methods return a promise which is fulfilled when the task completes.

That may be the basis of your problem. then and catch expect functions, not promises. The code I was referring to is:

server.doTask1()
  .then(server.doTask2())
  .then(server.doTask3())
  .catch( ... handle the error )

When that code runs, the following happens synchronously:

  1. server.doTask1 is called, starting its work and returning a promise (let's call it "p1")
  2. server.doTask2 is called, starting its work and returning a promise ("p2")
  3. then is called on "p1", passing in "p2"; it returns a promise ("p3")
  4. server.doTask3 is called, starting its work and returning a promise ("p4")
  5. then is called on "p3", passing in "p4"; it returns a promise ("p5")
  6. catch is called on "p5" (returning a promise that's never used)

At that point, the tasks started by doTask, doTask2, and doTask3 are all running in parallel; they'll settle their promises, but there's no order imposed between the tasks. You can see that here:

const start = Date.now();
const elapsed = () => String(Date.now() - start).padStart(4);
const log = msg => console.log(`[${elapsed()}] ${msg}`);

const server = {
    doTask1() {
        return new Promise(resolve => {
            log("task1 started");
            setTimeout(() => {
                log("task1 complete");
                resolve("result1");
            }, 200);
        });
    },
    doTask2() {
        return new Promise(resolve => {
            log("task2 started");
            setTimeout(() => {
                log("task2 complete");
                resolve("result2");
            }, 300);
        });
    },
    doTask3() {
        return new Promise(resolve => {
            log("task3 started");
            setTimeout(() => {
                log("task3 complete");
                resolve("result3");
            }, 100);
        });
    },
};

log("Running the code");
server.doTask1()
    .then(server.doTask2())
    .then(server.doTask3())
    .catch(() => log(`ERROR: ${error}`))
    .then(() => log("Chain complete"));
log("Done running the code");
.as-console-wrapper {
    max-height: 100% !important;
}

Notice how tasks 1-3 all start right away.

To make them run one after another, you need to pass a function, not a promise, into then:

server.doTask1()
  .then(() => server.doTask2())
  //    ^^^^^^
  .then(() => server.doTask3())
  //    ^^^^^^
  .catch( ... handle the error )

I've made two assumptions there:

  1. It matters what this doTask2 and doTask3 are called with
  2. You don't want the result from the previous task passed to the next one.

There are other ways you could write it if either or both of those isn't true.

Live Example:

const start = Date.now();
const elapsed = () => String(Date.now() - start).padStart(4);
const log = msg => console.log(`[${elapsed()}] ${msg}`);

const server = {
    doTask1() {
        return new Promise(resolve => {
            log("task1 started");
            setTimeout(() => {
                log("task1 complete");
                resolve("result1");
            }, 200);
        });
    },
    doTask2() {
        return new Promise(resolve => {
            log("task2 started");
            setTimeout(() => {
                log("task2 complete");
                resolve("result2");
            }, 300);
        });
    },
    doTask3() {
        return new Promise(resolve => {
            log("task3 started");
            setTimeout(() => {
                log("task3 complete");
                resolve("result3");
            }, 100);
        });
    },
};

log("Running the code");
server.doTask1()
    .then(() => server.doTask2())
    .then(() => server.doTask3())
    .catch(() => log(`ERROR: ${error}`))
    .then(() => log("Chain complete"));
log("Done running the code");
.as-console-wrapper {
    max-height: 100% !important;
}

Alternatively, in an async function:

try {
    await server.doTask1();
    await server.doTask2();
    await server.doTask3();
    // ...
} catch (error) {
    // ...handle/report error...
}

Live Example:

const start = Date.now();
const elapsed = () => String(Date.now() - start).padStart(4);
const log = msg => console.log(`[${elapsed()}] ${msg}`);

const server = {
    doTask1() {
        return new Promise(resolve => {
            log("task1 started");
            setTimeout(() => {
                log("task1 complete");
                resolve("result1");
            }, 200);
        });
    },
    doTask2() {
        return new Promise(resolve => {
            log("task2 started");
            setTimeout(() => {
                log("task2 complete");
                resolve("result2");
            }, 300);
        });
    },
    doTask3() {
        return new Promise(resolve => {
            log("task3 started");
            setTimeout(() => {
                log("task3 complete");
                resolve("result3");
            }, 100);
        });
    },
};

(async () => {
    try {
        await server.doTask1();
        await server.doTask2();
        await server.doTask3();
        // ...
        log("Chain complete");
    } catch (error) {
        // ...handle/report error...
        log(`ERROR: ${error}`);
    }
})();
.as-console-wrapper {
    max-height: 100% !important;
}

Note: There are some non-standard libraries that don't start the asynchronous work promised until you call then on the promise. Those are outliers. In the normal case, a promise is a completion token for work already in progress (and that's how async functions behave).

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
0

Use async/await properly and you're good to go:

async doLongRunningTask() {
    const result = await this.runCommand('external command')
    return this.waitForCommand(result.Id);
}

private async waitForCommand(commandId : string) : Promise<any> {
    while (true) {
        const result = this.runCommand(`external command status report ${commandId}`);
        if (result.Status==='IN_PROGRESS') {
            console.log(`Status is ${result.Status}, waiting...`);
            await this.wait(60);
        } else {
            console.log(`Status complete`);
            return;
        }
    } 
}

For that wait method, you need to actually do it asynchronously not with busy waiting. See Combination of async function + await + setTimeout and What is the JavaScript version of sleep()?:

public wait(seconds : number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, seconds*1000));
}

And finally, your promise chain needs to pass callback functions to .then() (as also @TJCrowder remarked - see his answer for alternative solutions):

server.doTask1()
  .then(() => server.doTask2())
  .then(() => server.doTask3())
  .catch(err => { /* ... handle the error */ });
Bergi
  • 630,263
  • 148
  • 957
  • 1,375