3

I have the following code. My intention is to start a task (which involves a few await calls before it can actually start). When it finishes starting it, I need to update the UI to tell user that the task has started and is waiting for result. Result will come in later so I want to return another Promise so the app knows when to update the UI with returned result:

async function startSomethingAsync() {
    await new Promise(r => globalThis.setTimeout(r, 1000));
    console.log("Task started");

    const finishedPromise = new Promise<number>(r => {
        globalThis.setTimeout(() => r(100), 3000);
    });
    return finishedPromise;
}

(async() => {
    // resultPromise is number instead of Promise<number>
    const resultPromise = await startSomethingAsync();

    // My intention: update UI after starting the task
    console.log("Task started");

    // Then when the result is there, do something else
    // Currently this is not compiled because resultPromise is not a Promise
    resultPromise.then(r => console.log("Result arrived: " + r));
})();

However the above code does not work. Somehow resultPromise is number (100 after resolved) instead of Promise<number>. TypeScript also recognize startSomethingAsync returning Promise<number> instead of Promise<Promise<number>>.

Why is this happening? Shouldn't the async method wrap another Promise outside of the returned Promise? How do I achieve what I am trying to do and is it a "good" pattern?


I even tried wrapping it by myself:

async function startSomethingAsync() {
    await new Promise(r => globalThis.setTimeout(r, 1000));
    console.log("Task started");

    const finishedPromise = new Promise<Promise<number>>(r1 => r1(new Promise<number>(r2 => {
        globalThis.setTimeout(() => r2(100), 3000);
    })));
    return finishedPromise;
}

The function still returns Promise<number> instead of Promise<Promise<number>>.

4
  • 1
    The docs clearly say that's it's an intended behavior developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…. "Return value: A Promise that is resolved with the given value, or the promise passed as value, if the value was a promise object." Commented May 10, 2022 at 21:08
  • @KonradLinkowski thanks, I didn't know that. Now that make sense. I wonder if there is a way to do what I am trying to do without callback. Commented May 10, 2022 at 21:09
  • 1
    return { promise: finishedPromise}; and then resultPromise.promise.then(.... Returning a function from the first function and then calling it in the second should also work return () => finishedPromise. then resultPromise().then(... Commented May 10, 2022 at 21:11
  • 1
    @KonradLinkowski that's smart! It solved my problem though I feel it's a bit weird. But if you write an answer, I will accept it Commented May 10, 2022 at 21:12

3 Answers 3

3

As we discussed in the comments - you have to wrap the resulting promise in a function or an object.

async function startSomethingAsync() {
  await new Promise(r => setTimeout(r, 1000));

  const finishedPromise = new Promise(r => {
    setTimeout(() => r(100), 3000);
  });
  return () => finishedPromise;
}

(async () => {
  const resultPromise = await startSomethingAsync();
  resultPromise().then((r) => console.log("Result arrived: " + r));
})();

Sign up to request clarification or add additional context in comments.

1 Comment

I find the simplest way to wrap the returned promise into an array: return [finishedPromise]; and then const [resultPromise] = await startSomethingAsync();
1

Why is this happening? Shouldn't the async method wrap another Promise outside of the returned Promise?

No. If you return a promise from an async function, the promise created by calling the async function is just resolved to that promise (it waits for that other promise to settle and takes its fulfillment or rejection as its own; more on promise terminology on my blog here). It doesn't set up the promise to be fulfilled with a promise. A promise is never fulfilled with a promise (regardless of async functions).

How do I achieve what I am trying to do and is it a "good" pattern?

If you want to use the promise from startSomethingAsync, simply don't await it:

(async() => {
    const resultPromise = startSomethingAsync();
    //                    ^−−− no `await`

    // My intention: update UI after starting the task
    console.log("Task started");

    // Wait until it's finished
    const r = await resultPromise;
    //        ^−−− *now* we `await` await it
    console.log("Result arrived: " + r);
})();

Note that if startSomethingAsync has an asynchronous delay before it starts its work (which would be highly unusual), the "Task started" log will occur before that asynchronous delay occurs. If you don't want that, separate out that part from the function so you can get your "Task started" logging in the middle. For instance, you might have a separate function that does the initial bit, and then you pass anything from that into startSomethingAsync:

(async() => {
    const resultPromise = startSomethingAsync(await theStuffBefore());
    //                                        ^^^^^

    // My intention: update UI after starting the task
    console.log("Task started");

    // Wait until it's finished
    const r = await resultPromise;
    //        ^−−− *now* we `await` await it
    console.log("Result arrived: " + r);
})();

Alternatively, if your process is multi-step, each step could return a function (or an object with methods) for the next step:

(async() => {
    const nextStep = await startSomethingAsync();
    const resultPromise = nextStep(); // No `await`

    // My intention: update UI after starting the task
    console.log("Task started");

    // Wait until it's finished
    const r = await resultPromise;
    console.log("Result arrived: " + r);
})();

(In fact, that's very similar to using an async generator.)

These are all variations on a theme, but the take-away message is that a promise can never be fulfilled with another promise. You'll notice that the standard name for the functions you get from new Promise are resolve and reject, not fulfill and reject. That's because when you pass a promise into resolve (or return one from an async function, which is effetively the same thing), it resolves the promise to that other promise, rather than fulfilling the promise with that other promise.

7 Comments

Sorry maybe I will clarify my question. There are two things here: I need to wait for the "starting" to finish (then update UI telling user that the task has started, but NOT finished). Then I wait for the result (i.e. task result came) and update UI again.
@LukeVo - Heh, I was just updating the answer to address that; see the edit. It's very unusual for an async function to have an asyncrhonous delay you want to wait for before returning control to the caller. Instead, break it up into two functions, or accept a callback to call in the middle (but I wouldn't go that route).
@LukeVo - In the worst case, you could have the function return a promise that gets fulfilled with an object wrapping another promise, but I've never seen anyone do that in production code and I'd be a hard sell on it in a code review. :-D
True, that's my worry as well. But I think that's how fetch does that. i.e. fetch(url).then(r => r.json()).then(json => console.log(json)) so I think it's not too bad.
@LukeVo - fetch doesn't do that. :-) It returns a Response object that has methods (not promises) as properties. It has to do that because there are different ways to consume the body of the response (if you even want to consume it -- for instance, you almost never actually read the body of a 404 response, or a 500 error). So it can't just do both the initial request and reading the body, because it doesn't know whether to read it as text, JSON (text + parsing), a stream, ... But yes, you could do something similar if you liked.
|
0

All async functions implicitly return a promise. If you explicitly return a promise, it will not be wrapped.

If a promise resolves another promise, they nested promises are flattened -

(new Promise(r =>
  r(new Promise(r2 =>
    r2("hello")
  ))
))
.then(console.log).catch(console.error)

2 Comments

I tried wrapping it by myself as well (see additional code I just added). It still "unwrap" it somehow. I never see any doc on the "auto"-unwrapping of async method.
When you return finishedPromise it is an ordinary promise and does not get wrapped. There's not nesting happening.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.