Creating Multi-Line Strings in JS With Backticks

Learn every technique for creating multi-line strings in JavaScript using template literals. Master backtick syntax, indentation handling, HTML templates, and whitespace management with practical examples.

JavaScriptbeginner
9 min read

Before ES2015, creating multi-line strings in JavaScript was awkward. You had to escape newlines with backslashes, concatenate strings with +, or join arrays. Template literal (backtick strings) solved this by letting you write strings that span multiple lines naturally, with the line breaks preserved exactly as written. This guide covers every multi-line string technique with a focus on template literals, including how to handle the indentation and whitespace issues that come with them.

If you are looking for a broader overview of template literals and strings, that guide covers interpolation, tagged templates, and string methods. This article focuses specifically on multi-line patterns.

The Old Way: Before Template Literals

Before ES2015, developers used three workarounds for multi-line strings, each with drawbacks:

String Concatenation

javascriptjavascript
const html = "<div>\n" +
  "  <h1>Welcome</h1>\n" +
  "  <p>Hello, World!</p>\n" +
  "</div>";

This works but is tedious and error-prone. Every line needs quotes, a + operator, and an explicit \n.

Backslash Line Continuation

javascriptjavascript
const html = "<div>\n\
  <h1>Welcome</h1>\n\
  <p>Hello, World!</p>\n\
</div>";

The backslash \ at the end of each line tells JavaScript to continue on the next line. However, it does not insert a newline character. You still need \n for actual line breaks. Adding a space after the \ breaks the continuation silently.

Array Join

javascriptjavascript
const html = [
  "<div>",
  "  <h1>Welcome</h1>",
  "  <p>Hello, World!</p>",
  "</div>",
].join("\n");

This is the cleanest pre-ES6 approach, but it still requires wrapping every line in quotes and managing the array structure.

MethodReadabilityError-PronePreserves Indentation
Concatenation (+)PoorYes (missing \n)Manual
Backslash (\)PoorYes (trailing spaces)No
Array .join()ModerateModerateManual
Template literals (`)ExcellentNoAutomatic

Template Literals: The Modern Way

Template literals preserve newlines exactly as written. No escape sequences, no concatenation, no arrays:

javascriptjavascript
const html = `<div>
  <h1>Welcome</h1>
  <p>Hello, World!</p>
</div>`;
 
console.log(html);
// <div>
//   <h1>Welcome</h1>
//   <p>Hello, World!</p>
// </div>

The string contains the exact whitespace and line breaks you see in the source code. This is both the feature and the challenge of template literals.

Practical Multi-Line Patterns

HTML Templates

Template literals excel at building HTML strings:

javascriptjavascript
function createCard(user) {
  return `
    <article class="user-card">
      <h2>${user.name}</h2>
      <p class="role">${user.role}</p>
      <p class="email">${user.email}</p>
      <span class="status ${user.active ? "active" : "inactive"}">
        ${user.active ? "Active" : "Inactive"}
      </span>
    </article>
  `.trim();
}
 
const card = createCard({
  name: "Ada Lovelace",
  role: "Engineer",
  email: "ada@runehub.dev",
  active: true,
});

Using .trim() at the end removes the leading and trailing newlines created by placing the backtick on its own line.

SQL Queries

javascriptjavascript
const query = `
  SELECT
    users.id,
    users.name,
    users.email,
    COUNT(orders.id) AS order_count
  FROM users
  LEFT JOIN orders ON orders.user_id = users.id
  WHERE users.active = true
  GROUP BY users.id
  ORDER BY order_count DESC
  LIMIT 10
`.trim();

Error Messages

javascriptjavascript
function validateAge(age) {
  if (typeof age !== "number" || age < 0 || age > 150) {
    throw new Error(`
Invalid age value: ${age}
 
Expected: a number between 0 and 150
Received: ${typeof age} with value ${JSON.stringify(age)}
 
Check the input source and ensure proper type conversion.
    `.trim());
  }
}

Configuration Strings

javascriptjavascript
function generateNginxConfig(domain, port) {
  return `
server {
    listen 80;
    server_name ${domain};
 
    location / {
        proxy_pass http://localhost:${port};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
  `.trim();
}

The Indentation Problem

Template literals preserve ALL whitespace, including the indentation from your source code. This causes issues when the template is nested inside functions or conditionals:

javascriptjavascript
function getWelcomeMessage(name) {
  if (name) {
    return `
      Welcome, ${name}!
      
      Here are your options:
        - View dashboard
        - Edit profile
        - Sign out
    `;
  }
}
 
console.log(getWelcomeMessage("Ada"));
// Output has 6 spaces of unwanted indentation on every line:
//
//       Welcome, Ada!
//       
//       Here are your options:
//         - View dashboard
//         - Edit profile
//         - Sign out
//     

The string contains the indentation from the source code because template literals do not strip leading whitespace.

Solution 1: Align to the Left Margin

javascriptjavascript
function getWelcomeMessage(name) {
  if (name) {
    return `Welcome, ${name}!
 
Here are your options:
  - View dashboard
  - Edit profile
  - Sign out`;
  }
}

This produces correct output but makes the source code ugly when the template is deeply nested.

Solution 2: Use .trim() and .replace()

javascriptjavascript
function getWelcomeMessage(name) {
  if (name) {
    return `
      Welcome, ${name}!
      
      Here are your options:
        - View dashboard
        - Edit profile
        - Sign out
    `.replace(/^ {6}/gm, "").trim();
  }
}

The regex ^ {6} matches exactly 6 leading spaces on each line (the unwanted indentation) and removes them.

Solution 3: Dedent Utility Function

A reusable dedent function strips the common leading whitespace from all lines:

javascriptjavascript
function dedent(strings, ...values) {
  // Build the full string
  let result = strings.reduce((acc, str, i) => {
    return acc + str + (values[i] !== undefined ? values[i] : "");
  }, "");
  
  // Find the minimum indentation (ignoring empty lines)
  const lines = result.split("\n");
  const nonEmptyLines = lines.filter((line) => line.trim().length > 0);
  const minIndent = nonEmptyLines.reduce((min, line) => {
    const indent = line.match(/^(\s*)/)[1].length;
    return Math.min(min, indent);
  }, Infinity);
  
  // Remove the common indentation and trim
  return lines
    .map((line) => line.slice(minIndent))
    .join("\n")
    .trim();
}
 
// Usage
function getWelcomeMessage(name) {
  return dedent`
    Welcome, ${name}!
    
    Here are your options:
      - View dashboard
      - Edit profile
      - Sign out
  `;
}
 
console.log(getWelcomeMessage("Ada"));
// Welcome, Ada!
// 
// Here are your options:
//   - View dashboard
//   - Edit profile
//   - Sign out
Dedent Libraries

For production use, the npm package dedent (or ts-dedent for TypeScript) provides a well-tested tagged template function that handles edge cases like tabs, mixed indentation, and expression alignment.

Escape Characters in Multi-Line Strings

Even with template literals, you sometimes need escape characters:

javascriptjavascript
// Escaping backticks inside template literals
const code = `Use \`backticks\` for template literals.`;
 
// Escaping ${} to show literal syntax
const docs = `Use \${variable} syntax for interpolation.`;
 
// Mixing escape sequences with literal newlines
const mixed = `Line 1
Line 2\tTabbed content
Line 3`;
Escape SequenceOutputWorks In Template Literals?
\nNewlineYes (but usually unnecessary)
\tTabYes
\\BackslashYes
\`BacktickYes (required inside template literals)
\${Literal ${Yes (prevents interpolation)

Building Dynamic Multi-Line Content

Lists from Arrays

javascriptjavascript
const features = ["Fast compilation", "Type safety", "Hot reload", "Built-in routing"];
 
const featureList = `
Key Features:
${features.map((f, i) => `  ${i + 1}. ${f}`).join("\n")}
`.trim();
 
console.log(featureList);
// Key Features:
//   1. Fast compilation
//   2. Type safety
//   3. Hot reload
//   4. Built-in routing

Conditional Sections

javascriptjavascript
function generateEmail(user, order) {
  return `
Hi ${user.name},
 
Thank you for your order #${order.id}.
 
${order.items.map((item) => `- ${item.name}: $${item.price.toFixed(2)}`).join("\n")}
 
Total: $${order.total.toFixed(2)}
${order.discount ? `\nDiscount applied: -$${order.discount.toFixed(2)}` : ""}
${order.notes ? `\nNotes: ${order.notes}` : ""}
 
Best regards,
RuneHub Team
  `.trim();
}

Best Practices

Multi-Line String Guidelines

Follow these practices for clean, maintainable multi-line strings.

Always call .trim() on multi-line template results. Starting and ending a template on their own lines creates leading/trailing newlines. .trim() removes them cleanly.

Use a dedent function for deeply nested templates. When templates appear inside functions, conditionals, or classes, a dedent utility keeps the source code readable while producing correctly indented output.

Keep template literals readable in source code. If a template is longer than 15-20 lines, consider moving it to a separate function or file. Long inline templates make the containing function hard to read.

Use .join() for dynamic line generation. When building lists from arrays, array.map().join("\n") is more readable and maintainable than manual string concatenation.

Test multi-line output explicitly. Whitespace bugs are invisible in console.log(). Use assertions that check for exact string content, including indentation.

Common Mistakes and How to Avoid Them

Multi-Line Pitfalls

Template literal whitespace issues are among the most common formatting bugs in JavaScript applications.

Including source indentation in the output. The most common template literal mistake. Always test the actual output string, not just how it looks in the console. Use .trim() and dedent patterns.

Forgetting that empty lines have whitespace. An "empty" line inside a template literal may contain spaces from your source indentation. This can break text processing that checks for truly empty lines with line === "".

Using \n when a literal newline works. Inside template literals, just press Enter for a new line. Using \n inside backtick strings adds unnecessary noise.

Mixing tabs and spaces in templates. If your editor uses tabs for indentation but the template literal needs spaces (or vice versa), the output will have inconsistent whitespace. Configure your editor to use consistent whitespace.

Next Steps

Master template literal interpolation

Explore advanced interpolation patterns including nested templates, conditional expressions, and function calls inside ${}.

Learn tagged templates

Build custom tagged template functions for SQL sanitization, HTML escaping, and internationalization.

Explore [JavaScript operators](/tutorials/programming-languages/javascript/js-operators-arithmetic-logical-comparison)

Understand how string operators like + and comparison operators interact with template literal output.

Build an email template system

Create a reusable email generator using template literals with conditional sections, dynamic lists, and the dedent pattern for clean formatting.

Rune AI

Rune AI

Key Insights

  • Backticks preserve line breaks: template literals include newlines exactly as written in source code
  • Always use .trim(): multi-line templates create leading/trailing whitespace that needs removal
  • Dedent for nested templates: a dedent utility strips unwanted source indentation from output
  • Use .map().join() for dynamic lists: this pattern is cleaner and more maintainable than loop concatenation
  • Whitespace is invisible: always test multi-line output explicitly, not just visual console output
RunePowered by Rune AI

Frequently Asked Questions

How do I create a multi-line string in JavaScript?

The simplest way is to use template literals (backticks). Write your string between `` ` `` characters and press Enter for new lines. The line breaks are preserved exactly as written. For example: `` `Line 1\nLine 2\nLine 3` `` creates a three-line string.

Do template literals preserve whitespace and indentation?

Yes, template literals preserve ALL whitespace exactly as it appears in the source code, including spaces and tabs used for code indentation. This means strings nested inside functions or conditionals will contain the surrounding indentation. Use `.trim()` to remove leading/trailing whitespace, or a dedent utility to strip common indentation.

What is the dedent pattern for template literals?

The dedent pattern uses a tagged template function that automatically removes the common leading whitespace from all lines in a multi-line template literal. This lets you write indented template literals in your source code (for readability) while producing clean output without the extra indentation. The npm `dedent` package provides a production-ready implementation.

Can I use variables inside multi-line template literals?

Yes. Template literal interpolation (`${expression}`) works exactly the same in multi-line strings as in single-line ones. You can embed variables, function calls, [ternary operator](/tutorials/programming-languages/javascript/javascript-ternary-operator-complete-syntax-guide)s, and any other JavaScript expression inside `${}` at any point in a multi-line template.

How do I escape backticks inside a template literal?

Use a backslash before the backtick: `` \` ``. This prevents the backtick from ending the template literal. Similarly, use `\${` to include a literal `${` without triggering interpolation.

Conclusion

Template literals made multi-line strings a first-class feature in JavaScript, replacing the error-prone concatenation and backslash approaches of the past. The key to using them well is managing whitespace: call .trim() on results, use a dedent utility for nested templates, and build dynamic content with .map().join("\n") instead of manual string concatenation. These patterns produce clean output while keeping your source code readable and maintainable.