Before I get flamed for using eval()... YES, I know eval === evil and from the devil and should never be used. That is NOT the question here. Believe it or not, I actually have a valid, real life case where it makes perfect sense to use eval, as a matter of fact, there is no other way to achieve the result I require.
Below is a code snippet with 4 cases. The first three works exactly as expected. The first two calls an async function in a template literal. The 3rd and 4th stores the template literal as a string and then eval the string. In the case (no 3) where await is not used, as expected the promise is returned. The problem is however case 4. It throws an error and for the life of me, I can't figure out why, or how to get around it.
I'm hoping someone smarter than me can either come up with a solution so that case 4 can still be evaluated without throwing an error (first prize) or explain why it doesn't work/can't be done.
async function foo(str) {
return str.toUpperCase();
}
async function bar() {
const str1 = `${foo("xxx")}`;
console.log("STR1:", str1);
// Output: STR1: [object Promise]
const str2 = `${await foo("xxx")}`;
console.log("STR2:", str2);
// Output: STR2: XXX
const str3 = '`${foo("xxx")}`';
console.log("STR3:", await eval(str3));
//Output STR3: [object Promise]
const str4 = '`${await foo("xxx")}`';
console.log("STR4:", await eval(str4));
// undefined:1
// `${await foo("xxx")}`
// ^^^^^^
// Output: SyntaxError: Missing } in template expression
}
bar();
Another way of making it work, basically by separating the awaiting of the promise from the construction of the string (kind of), using tagged templates is described below:
async function foo(str) {
return str.toUpperCase();
}
async function asyncFooTmpl(strings, asyncExpr) {
return `${strings[0]}${await asyncExpr}${strings[1]}`;
}
async function bar() {
const str1 = `${foo("xxx")}`;
console.log("STR1:", str1);
// Output: STR1: [object Promise]
const str2 = `${await foo("xxx")}`;
console.log("STR2:", str2);
// Output: STR2: XXX
const str3 = '`${foo("xxx")}`';
console.log("STR3:", await eval(str3));
//Output STR3: [object Promise]
const str4 = 'asyncFooTmpl`${foo("xxx")}`';
console.log("STR4:", await eval(str4));
//Output STR4: XXX
}
bar();
This transfers the ownership of awaiting the promise from bar to asyncFooTmpl, allowing the string to be produced.
That said, it would require you to write functions for every template you have, or create a function that makes dealing with tagged templates more manageable.
Since you accepted this, and wrote your own answer combining my comment, I created a fiddle of a somewhat more improved example if you're curious.
I've tried many approaches and while I often came close to a solution, it never quite worked. The solution proposed by Heretic Monkey worked like a charm. I literally copied his code for "genericizing" the template function and it worked. Did not need to change a thing.
In case someone else is looking for a solution such as this, I'm posting the full example I put together to prove to myself that it works. The example is a bit long but I wanted to show a practical, real world use of the solution to make it easier of others to understand.
async function foo(str) {
await sleep(2000);
console.log(`foo caled with ${str}`);
return str.toUpperCase();
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function resolveTemplate(strings, ...maybePromises) {
const final = [];
for (let i = 0; i < strings.length; i++) {
final.push(strings[i]);
final.push(Object.prototype.toString.call(maybePromises[i]).includes("Promise") ? await maybePromises[i] : maybePromises[i]);
}
return final.join("");
}
const person = { firstName: "Joe", lastName: "Blogs", Age: 23 };
async function bar() {
let str = 'resolveTemplate`{ "name": "${foo(person.firstName.concat(" ",person.lastName))}", "age": ${person.Age}, "sex": "${foo("male")}" }`';
console.log("STR4:", await eval(str));
//Output STR4: { "name": "JOE BLOGS", "age": 23, "sex": "MALE" }
}
bar();
console.log("Done");
It's not related to eval directly. You just can't use await outside of async function.
This will result in the same error:
const x = `${await Promise.resolve()}`
Wrapping the call in async IIFE solves the issue:
async function foo(str) {
return str.toUpperCase();
}
async function bar() {
const x = '`${(async () => await foo("xxx"))()}`'
eval(x)
}
bar();
Proof that await can't be used in eval in async function, because eval isn't that simple
async function foo(str) {
return str.toUpperCase();
}
async function bar() {
try {
const x = ' await foo("xxx")`'
eval(x)
} catch (e) {
console.error(e)
}
}
bar();