JAVASCRIPT / STRINGS AND TEMPLATE LITERALS
Template literals and embedded expressions
Build strings with ${} placeholders that run any JavaScript expression, and predict how each value is converted and when the substitution happens.
What you will learn
- Insert any expression with ${}: calls, arithmetic, ternaries, nested templates
- Predict conversions: arrays join with commas, plain objects give [object Object]
- Escape a literal placeholder with \${ and an inner backtick with a backslash
- Re-evaluate a template by wrapping it in a function instead of rebuilding the string
Understanding Template literals and embedded expressions
A template literal is not a string with variables inside it; it is a short recipe the engine follows. Backticks split the text into fixed chunks and holes, each hole written as ${...}. When execution reaches the literal, JavaScript evaluates the expression in each hole, converts each result to a string, and concatenates chunk, value, chunk, value in source order. What comes out is an ordinary string, indistinguishable from one typed with quotes.
A hole needs an expression, meaning anything that produces a value. Arithmetic, comparisons, property lookups, method calls, ternaries and even another template literal all qualify, which is why ${a > b ? `${a} wins` : "tie"} is legal: the nested literal is itself just an expression. Statements do not qualify, because if, for, const and return produce no value to splice in, so writing one inside a hole is a SyntaxError raised before any code runs. When you need repetition, compute the text outside the literal or with map and join, then drop that single result into one hole.
Conversion follows the same rules as String(value), which trips people up more often than the syntax does. null and undefined print as the words null and undefined rather than vanishing, arrays print their elements joined by commas because Array.prototype.toString delegates to join, and a plain object prints [object Object] because it inherits the generic Object.prototype.toString. Substitution also happens exactly once, at the moment the literal is evaluated, so the result is a snapshot rather than a live view. Reassigning a variable afterwards cannot change a string that has already been built.
const item = "cortado";
const price = 3.5;
const qty = 3;
console.log(`${qty} x ${item} @ $${price.toFixed(2)} = $${(qty * price).toFixed(2)}`);
const stock = 0;
console.log(`Status: ${stock > 0 ? `${stock} left` : "sold out"}`);
console.log(`Mixed: ${[1, 2]} | ${{ a: 1 }} | ${null} | ${undefined}`);A ${...} hole holds an expression, not a variable name: it is evaluated once, converted to a string, and spliced into the fixed text around it.
Worked examples
Snapshot, not a live binding
Shows that a hole is evaluated where the literal appears, so later changes to the variable do not affect the finished string.
let count = 1;
const snapshot = `count is ${count}`;
const live = () => `count is ${count}`;
count = 99;
console.log(snapshot);
console.log(live());Example explained
Line 1The hole in snapshot runs on line 2, so it captures the value 1 and produces the characters "count is 1".
Line 2live stores the same literal inside a function body, so nothing is evaluated until the call on the last line.
Line 3Reassigning count cannot alter snapshot, because the built string is plain characters with no link back to the variable.
Nesting a template inside a hole
Builds a multi-line report by computing the middle section with map and a nested template literal, then splicing it into one outer literal.
const cart = [
{ name: "mug", qty: 2, price: 8 },
{ name: "beans", qty: 1, price: 14.5 },
];
const lines = cart.map(i => ` ${i.name.padEnd(6)} ${i.qty} @ ${i.price}`).join("\n");
const total = cart.reduce((sum, i) => sum + i.qty * i.price, 0);
console.log(`Cart:\n${lines}\nTotal: ${total.toFixed(2)}`);Example explained
Line 1i.name.padEnd(6) runs inside the hole, so column alignment happens during substitution instead of in a temporary variable.
Line 2The inner literal is an expression, which is why it can be the body of the arrow function feeding the outer literal's hole.
Line 3lines already contains newline characters, and it is inserted as one multi-line chunk by a single hole.
Line 4total.toFixed(2) returns a string, so 30.5 prints as 30.50 rather than 30.5.
Escaping dollars and backticks
Shows when a dollar sign starts a placeholder and how to print ${...} or a backtick as literal text.
const total = 5;
console.log(`Cost: $${total}`);
console.log(`Not a hole: \${total}`);
console.log(`Bare $ or { is fine: $ {`);
console.log(`Backtick: \``);Example explained
Line 1In $${total} the first dollar is literal text and the second begins the hole, so the currency symbol survives.
Line 2Escaping the dollar with a backslash (\${) makes the engine treat the following brace as ordinary text, printing ${total}.
Line 3A dollar not followed by a brace, and a brace on its own, need no escaping at all.
Line 4A backtick inside the literal must be escaped, otherwise it would close the literal early.
Important notes
Template literals perform no escaping, so element.innerHTML = `<p>${comment}</p>` with user input is a script-injection hole; use textContent or an escaping helper.
A symbol is the one value that refuses this implicit conversion: ${Symbol("id")} throws a TypeError even though String(Symbol("id")) returns "Symbol(id)".
Common mistakes
Writing 'Hi ${name}' with single or double quotes: the placeholder is printed verbatim and no error is raised, so the bug surfaces in the UI rather than the console.
Putting a statement in a hole, such as ${if (n > 0) "yes"}: that is a SyntaxError, so the whole file fails to parse and nothing in it runs, not just that line.
Interpolating a whole object or array of objects: ${user} gives [object Object] and ${users} gives [object Object],[object Object], hiding the data you meant to show.
Try it yourself
Change, predict, then run
In a browser console, declare const name = "Ada" and let score = 92, then log a single template literal that reads Ada scored 92% (pass), using a ternary that switches to (fail) under 60. Set score = 45, re-run the log line, and confirm only the label changed.
Open the JavaScript workspaceCheck your understanding
Running let n = 2; const s = `n = ${n * 2}`; n = 10; console.log(s); what appears in the console?
- n = 20, because the hole is re-evaluated each time the string is used
- n = 4, because the hole ran where the literal appeared and the result is a fixed string
- n = ${n * 2}, because a const string is not interpolated
- A TypeError, because s was built from a variable that later changed
Show answer
Substitution happens once, on the line where the literal is evaluated: n * 2 was 4 then, so s holds the characters "n = 4" and keeps no reference to n, making the later assignment invisible to it. Choosing n = 20 assumes the template stays live and is re-read at log time, which is how some templating engines behave but not JavaScript literals; getting 20 requires evaluating the literal again, for example inside a function.