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