Learn why TypeScript throws TS1160 when a template literal is never closed, and how to find the missing backtick or the unclosed placeholder.
TS1160 means the TypeScript scanner found a template literal that was opened with a backtick but never closed. Somewhere above the reported line there is a ` — or a ${ placeholder — that has no partner, and the compiler ran out of file looking for it.
This is a scanner error, raised before parsing finishes and long before type checking starts. No compiler option changes it: strict, target, module and skipLibCheck are all irrelevant, because the file cannot be turned into tokens in the first place. The rule comes from JavaScript itself and TypeScript has enforced it since template literals arrived in version 1.4.
The part that makes TS1160 confusing is where it is reported. Unlike a plain string, a template literal may legally contain real line breaks, so an unclosed one keeps consuming your code — every following statement becomes string content — until the file ends. The error then lands on the last line.
// The general shape of the error:
const greeting = `Hello, ${userName}
const retryCount = 1
//
// ~ Error at line 4, column 1: Unterminated template literal.Read that as "the template opened somewhere above is still open here", not as "line 4 is wrong". The fastest way to find the real position is syntax highlighting: from the offending backtick downwards, your editor colours the entire file as a string.
The everyday case. You type the opening backtick, interpolate a value, and never come back for the closing one — often because the closing } of the placeholder looks like the end of the literal.
// ❌ Broken
const userName = "Dana"
const greeting = `Hello, ${userName}
const retryCount = 1
// Error: Unterminated template literal. (reported at the end of the file)// ✅ Fixed — close the template right after the placeholder
const userName = "Dana"
const greeting = `Hello, ${userName}`
const retryCount = 1Note that the compiler prints exactly one error here. Everything below the backtick was absorbed into the string, so there is no cascade to distract you — just a single complaint at a line that has nothing to do with the mistake.
A ${ opens a substitution that must be closed with } before the template can continue. Drop that brace and the scanner stays inside the expression, never returns to template mode, and hits the end of the file with the literal still open.
// ❌ Broken
const baseUrl = "https://api.example.com"
const orderId = "4821"
const orderUrl = `${baseUrl}/orders/${orderId`
// ~ the second placeholder never closes
// Error: Unterminated template literal.// ✅ Fixed — close the placeholder, then the template
const baseUrl = "https://api.example.com"
const orderId = "4821"
const orderUrl = `${baseUrl}/orders/${orderId}`URL building is where this shows up most, because the line is already dense with slashes and braces. Count the ${ openings and the } closings on the line: they must match before the final backtick.
Templates are the natural home for Markdown, SQL and shell snippets — all of which use backticks themselves. Inside a template, an unescaped backtick is the closing backtick, so it ends the literal early and the next one re-opens a new literal that runs off the end of the file.
// ❌ Broken
const usageHint = `Escape a backtick as ` inside a template.`
// ~ this one closes the template early
// ~ this one re-opens it
// Errors: several TS1005 'expected' errors on this line, then
// Error: Unterminated template literal.// ✅ Fixed — escape the literal backtick
const usageHint = `Escape a backtick as \` inside a template.`
// ✅ Also fine — a quoted string may contain backticks unescaped
const usageHintAlt = "Escape a backtick as ` inside a template."This is the one cause that produces a cascade: the fragment between the two stray backticks is parsed as code, so you get a handful of TS1005 errors on that line before the TS1160 at the bottom. Ignore them and fix the backticks.
Templates nest — a placeholder may contain another template. When you build markup with map() and a nested literal, there are two closing backticks to remember, and the outer one is the easy one to forget.
// ❌ Broken
const rows = ["Keyboard", "Monitor"]
const listMarkup = `<ul>${rows.map((row) => `<li>${row}</li>`).join("")}</ul>
// ~ no closing backtick
// Error: Unterminated template literal.// ✅ Fixed — close the outer template too
const rows = ["Keyboard", "Monitor"]
const listMarkup = `<ul>${rows.map((row) => `<li>${row}</li>`).join("")}</ul>`Because the inner literal is balanced, nothing looks wrong at a glance, and editor highlighting is less helpful than usual: the colours flip at the nested backtick rather than at the real problem. When a line has more than two backticks, count them — the total must be even.
Ignore the reported line and scroll up. TS1160 is always reported where the file ends, not where the template starts. Look for the first line whose syntax highlighting turns string-coloured and stays that way — the unmatched backtick is on it.
Balance the backticks, then the placeholders. Count the unescaped ` characters in the suspect region: an odd number means one is missing. Then check that every ${ has a matching } — an unclosed placeholder keeps the literal open just as effectively as a missing backtick.
Escape backticks that belong to the text. Write \` inside the template, or move the text into a single- or double-quoted string, which may contain backticks freely. When the payload is Markdown or shell script with many of them, a quoted string plus \n, or an array of lines joined with .join('\n'), is far less fragile than escaping each one.
Let a formatter find it for you. Prettier refuses to format a file it cannot parse and reports the position it gave up at, which is usually much closer to the real mistake than the compiler's end-of-file position. Running the formatter on save catches this before it ever reaches a build.
Don't reach for an escape hatch. as any, @ts-ignore and loosening tsconfig.json cannot help: the file never becomes valid tokens, so the suppression comment itself may not even be recognised. There is no configuration that makes TS1160 go away.
If the error points into node_modules, upgrade instead of editing. skipLibCheck skips type checking of declaration files, not syntax errors, so a malformed .d.ts shipped by a dependency still fails the build. Bump the package, or bump TypeScript — older compilers mis-scanned escaped backticks in some positions, and a generated file that is fine on a current version can break on an old one. The same applies to files produced by a code generator: fix the generator's escaping rather than patching its output.
TS1160 is raised by the scanner when a backtick-delimited template literal is still open at the end of the file. In practice it comes from one of four things: a forgotten closing backtick, a ${ placeholder that was never closed with }, a literal backtick inside the text that was not escaped, or a nested template where the outer literal lost its closing backtick.
No compiler flag affects it. Template literals have behaved this way since TypeScript 1.4, and because the failure happens during tokenization, nothing in tsconfig.json — including strict and skipLibCheck — changes the outcome.
Because a template literal is allowed to contain real line breaks. When the scanner passes an unmatched backtick it does not see a mistake; it sees the beginning of a long string, and it keeps reading. Every statement below becomes string content. Only when there is no input left does it conclude that the literal was never terminated, and the position it reports is that end-of-file position.
This is exactly where TS1160 differs from TS1002, "Unterminated string literal". A '…' or "…" string may not span lines, so TS1002 is reported on the line that contains the mistake. Template literals may span lines, so the report drifts to the bottom of the file:
const releaseNote = `Shipped on Monday.
// every line from here down is part of the stringTreat the reported line as a symptom and search upwards.
Escape it with a backslash — \` — exactly as you would escape a quote inside a quoted string. The escape is consumed by the parser, so the resulting value contains a plain backtick:
[object Object]If the text is full of backticks, such as a Markdown code fence or a MySQL query that quotes identifiers, escaping every one becomes noisy. Use an ordinary double-quoted string instead, since backticks carry no meaning there, and add \n where you need line breaks. Note that String.raw does not help: it keeps the backslash in the output, so you get a literal \` rather than the backtick you wanted.
Browse all TypeScript practice challenges to keep sharpening your type-level skills.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges