How to return a Promise from async function?

2019-07-11 21:59发布

问题:

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.

回答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);
}


回答2:

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);
}