2

When I try to return a promise from an async function, it's impossible to distinguish the status of the returned Promise and the function.

I think, the simplest solution is to put the promise to be returned in an array. The following is a stupid example, but I hope it demonstrates the problem:

function loadMetaData(id) {/*...*/} // Returns Promise<MetaData>
function loadSingleData(name) {/*...*/} // Returns Promise<SingleData>

async function startLoadingSingleData(id, object) {
    const metaData = object.metaData = await loadMetadata(id);
    const singleDataPromise = loadSingleData(metaData.dataToLoad);
    singleDataPromise.then(metaData => object.metaData = metaData);
    return [singleDataPromise];
}

async function logData(id) {
    const object = new SomeUsefulClassWhatTakesAdvantageOfMetadataProp();
    somePromise = (await startLoadingSingleData(id))[0];
    // Now metadata surely loaded, do something with it
    console.log(object.metaData);
    // But somedata will be loaded only in future
    somePromise.then(singleData => console.log(singleData));

    // And maybe: (depends of use-case)
    await somePromise;
}

When executing logData(/*...*/), first the metaData of the given ID of the given data after a short period, and after a little waiting the full singleData is expected.

But this is kinda hackish.

What is the intended way to overcome this situation?

PS.: This problem occurs too, when I try to return a Promise which resolves with the promise.

lezsakdomi
  • 115
  • 10
  • 2
    "t's impossible to distinguish the status of the returned Promise and the function" — I have no idea what you mean by this. – Quentin Mar 28 '18 at 18:24
  • "the simplest solution is to put the promise to be returned in an array" — Async functions must return a promise, not an array, not even if that array contains a promise. – Quentin Mar 28 '18 at 18:25
  • async functions return a Promise automatically. Whatever value you return from your async function is going to be wrapped in a Promise automatically. If you literally do `async function() { return new Promise(...) }`, you are actually returning a Promise of a Promise. Unless that's what you need, you don't need to create the promise yourself, just return your value. – matmo Mar 28 '18 at 18:32
  • 1
    Thank all of you, updated. I hope now this clarifies the need of returning a promise, and what I mean by it. – lezsakdomi Mar 28 '18 at 19:02

2 Answers2

3

Yes, unfortunately JS promises are not algebraic and you cannot fulfill a promise with another promise. There's no way around that (other than not using native promises, and not using async/await).

The easiest and most common solution is indeed using a wrapper object. It comes naturally to your problem:

// takes an id, returns a Promise<{metaData: Data, singleDataPromise: Promise<Data>}>
async function startLoadingSingleData(id) {
    const object = new SomeUsefulClassWhatTakesAdvantageOfMetadataProp();
    object.metaData = await loadMetadata(id);
//                    ^^^^^
    object.singleDataPromise =   loadSingleData(object.metaData.dataToLoad);
//                             ^ no await here
    return object;
}
async function logData(id) {
    const object = await startLoadingSingleData(id));
    // Now metadata surely loaded, do something with it
    console.log(object.metaData);
    // But some singleData will be loaded only in future
    const singleData = await object.singleDataPromise;
    console.log(singleData);
}

Notice that this potentially leads to problems with unhandled rejections if there is an exception in your code and you never get to await the singleDataPromise.

The (probably much better) alternative is to restructure your functions so that you don't create any promises before using (i.e. awaiting) them, like @Paulpro also suggested. So you'd just write a single strictly sequential function

async function logData(id) {
    const object = new SomeUsefulClassWhatTakesAdvantageOfMetadataProp();
    object.metaData = await loadMetadata(id);
    // Now metadata surely loaded, do something with it
    console.log(object.metaData);
    // But some singleData will be loaded only in future
    object.singleData = await loadSingleData(object.metaData.dataToLoad);
    console.log(object.singleData);
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
1

Your problem is that startLoadingSingleData has too many responsibilities. It is responsible for both loading the metadata and triggering loading of singledata.

Your logData function uses await startLoadingSingleData(id) as a way to make sure that metadata is available, which does not seem very intuituve. It is not obvious that startLoadingSingleData(id) returns a Promise that resolves when the metadata has loaded, and would be quite confusing for a coder looking at it for the first time (or yourself after a few months). Code should be self-documenting as much as possible so that you don't need to explain every line with comments.

My recommendation is to completely remove the startLoadingSingleData function and just do this inside logData:

async function logData(id) {
    const metaData = await loadMetadata(id);
    console.log(metaData);

    const singleData = await loadSingleData(metaData.name);
    console.log(singleData);
}

or if you don't want logData to await the SingleData Promise:

async function logData(id) {
    const metaData = await loadMetadata(id);
    console.log(metaData);

    loadSingleData(metaData.name).then( singleData => {
        console.log(singleData);
    } );
}

If you really want to keep using the function startLoadingSingleData instead then I think you should make it return an array or an object containing two Promises:

function startLoadingSingleData(id) {
    const metaDataLoaded = loadMetadata(id);
    const singleDataLoaded = metaDataLoaded.then(
      metaData => loadSingleData(metaData.dataToLoad)
    );

    return { metaDataLoaded, singleDataLoaded };
}

Then your usage would look something like:

async function logData(id) {
    const { metaDataLoaded, singleDataLoaded } = startLoadingSingleData(id);

    const metaData = await metaDataLoaded;
    console.log(metaData);

    const singleData = await singleDataLoaded;
    console.log(singleData);
}
Paul
  • 139,544
  • 27
  • 275
  • 264
  • OK, I forgot: there [is a problem with the final snippet](https://stackoverflow.com/questions/46889290/waiting-for-more-than-one-concurrent-await-operation) that does `a, b = getPromises(); await a; await b;`. When `loadMetaData` rejects, you would get an unhandled promise rejection from the `singleDataLoaded` promise :-( – Bergi Mar 28 '18 at 19:50