tl;dr I'm trying to return a data object in a synchronous function (which cannot be declared async
due to the API constraints), but some of my data is in the form of Promise
objects. I can delay the callback until after the promises are resolved with Promise.all([<promises>].then(callback)
, but I'm still returning a data object full of Promise
s. How do I replace all Promises with their resolved values in the returned data object without having to go through every last item one-by-one?
Background
In this specific case, I'm trying to write a Chrome extension. If you aren't aware, Chrome extensions are divided into two parts: the background "service worker" that acts as the central extension script, and the "content scripts" that actually act on each page. There are many things that can only be done from the background script, such as getting the current tab of the window you're operating on. This necessitates a way to call functions from the content script through the message API. I'm limiting my frustration at this to a minimum for the purposes of this question.
Details
My current API looks something like this:
chrome.runtime.onMessage.addListener(messageHandler)
// Example "message" object: { get: { globalFunctions: ['getCurrentTab'] } }
function messageHandler(message, sender, callback) {
const responseObject = {}
const promises = []
if (message.get) {
const get = message.get
const res = {}
if (get.globalFunctions) {
const funcNames = conformToStringArr(get.globalFunctions)
if (!funcNames)
return;
res.globalFunctions = {}
funcNames.forEach((funcName) => {
res.globalFunctions[funcName] = globalFunctions[funcName](message, sender) // Functions can be async because they have to asynchronously retrieve values
})
}
if (get.globalValues) {
const globalNames = conformToStringArr(get.globalValues)
if (!globalNames)
return;
res.globalValues = {}
globalNames.forEach((valueName) => {
res.globalValues[valueName] = globalValues[valueName]
})
}
if (get.localStorage) {
const toGet = conformToStringArr(get.localStorage)
if (!toGet)
return;
res.localStorage = chrome.storage.local.get(toGet) // : Promise
}
responseObject.get = res
}
callback(responseObject)
return true; // Required due to restrictions of the message API. I cannot return a Promise or use an asynchronous function.
}
Essentially, I'm doing this as though it were synchronous: filling an object with the relevant values and returning it. The problem is that sometimes these values are Promises, and Promise references do not automatically resolve to their values as far as I am aware. I'm starting to get a handle on how Promises work in terms of control-flow, but this kind of value-replacement is something that no one seems to mention online.
One thing I could do is only return the object once all of the promises are fulfilled by adding all Promise
s to an array and end the function with Promise.all(promises).then(callback)
, but that will still return an object full of Promises, right?
How do I return an object free of Promises from a non-async
function like this?
SOLUTION
Wrap the assignment in a Promise
instead. For example, instead of doing outObject.bar = asyncFoo()
(where asyncFoo
returns a Promise
), do asyncFoo().then((value) => outObject.bar = value)
. To only return the object once all of those values have been filled in, add each promise to a "promises" array and put Promise.allSettled(promises).then(callback(outObject))
at the end of the function.
Final code:
function messageHandler(message, sender, callback) {
const responseObject = {}
const promises = []
if (message.get) {
const get = message.get
responseObject.get = {}
if (get.globalFunctions) {
const funcNames = conformToStringArr(get.globalFunctions)
if (funcNames) {
responseObject.get.globalFunctions = {}
funcNames.forEach((name) => {
const functionResult = globalFunctions[name](message, sender)
if (functionResult instanceof Promise)
promises.push(functionResult.then((value) => responseObject.get.globalFunctions[name] = value))
else
responseObject.get.globalFunctions[name] = functionResult
})
}
}
if (get.globalValues) {
const globalNames = conformToStringArr(get.globalValues)
if (globalNames) {
responseObject.get.globalValues = {}
globalNames.forEach((valueName) => {
responseObject.get.globalValues[valueName] = globalValues[valueName]
})
}
}
if (get.localStorage) {
const toGet = conformToStringArr(get.localStorage)
if (toGet)
promises.push(chrome.storage.local.get(toGet).then((value) => responseObject.get.localStorage = value))
}
}
if (promises.length > 0) {
Promise.allSettled(promises).then(() => callback(responseObject))
} else {
callback(responseObject)
}
return true;
}