Skip to content

Error codes ​

HtmlError.code is the same number in every build. The development build writes the message out in full; the production build's message is E followed by the code.

codemeaningfrom
2an object or array for an attribute other than class, style, aria, dataattrs
3a refused attribute: anything starting with onhtml, attrs
5an unquoted attribute value before an expressionhtml
6a non-Html expression inside a tag, <script>, <style> or a commenthtml, frame
7a value that cannot be rendered: an object, a Promise, a symbolhtml
8a tag never closed with >html
9an element still open at the end, or one the next start tag closed implicitlyhtml
10an end tag that closes nothing, or the wrong elementhtml
11/> on an element that does not self-closehtml
12an end tag on a void elementhtml
13nesting the browser rewriteshtml
14the same attribute twice on one taghtml
15an id reference with no matching idcheck
16an id used twicecheck
17a bad tag nameframe, element, wrap
18a body on a void elementframe, element
19a URL the guard blockedcheck
20a value trusted would write differently from htmltrusted

Codes 1 and 4 are reserved.

When they happen. Codes 2, 3, 5, 6 and 7 throw the first time a template runs. Codes 8 to 14 throw at the same moment, but only in the development build. Codes 15, 16 and 19 are never thrown — check() returns them. Codes 17 and 18 throw whenever the offending entry is rendered. Code 20 throws only in the development build, whenever trusted gets a value that html would escape or block.

What never throws. Data. A hostile URL is replaced, hostile text is escaped, and neither stops the render. The one exception is trusted in the development build, whose job is to refuse a value that needs either.

Code 2 ​

An object or array given for an attribute that does not take one.

ts
attrs({ title: { a: 1 } }); // ✗
attrs({ class: ['a', 'b'], style: { color: 'red' } }); // fine

Four keys take objects or arrays: class, style, aria and data. Everything else takes a string, number, bigint, boolean, null or undefined.

Code 3 ​

An attribute starting with on, anywhere, with any value.

ts
html`<button onclick="${handler}">…</button>`; // ✗
attrs({ onclick: handler }); // ✗

The value is code, so no escaping makes it safe — and that holds for null and for Html too, so the refusal is unconditional. Attach behaviour with addEventListener; see in a browser.

WARNING

The check is a prefix, so a custom element's one or online attribute is refused as well. A prefix is the only thing that can be relied on here. Rename the attribute, or write the whole tag inside raw().

Code 5 ​

An expression in an attribute value with no quotes around it.

ts
html`<a href=${url}>…</a>`; // ✗
html`<a href="${url}">…</a>`; // fine

An unquoted value ends at the first whitespace, so a space anywhere in the value starts a new attribute. Escaping cannot fix that — the quotes have to be there.

Code 6 ​

An expression that is not Html in a context where only markup can go.

ts
html`<input ${flag}>`; // ✗ inside a tag
html`<script>${code}</script>`; // ✗ inside a script
html`<!-- ${note} -->`; // ✗ inside a comment
html`<${name}>`; // ✗ right after `<`, where the tag's name goes

Right after a < counts as inside a tag: a value that starts with a letter would be read as the tag's name, so img src=x onerror=… would open an <img> of its own. Write &lt; for a literal less-than sign, or put a trusted tag name in raw().

A <script> or <style> with a <!-- or <![CDATA[ still open counts as inside it, past its end tag: in an SVG script, and in the escaped states of an HTML one, the browser reads that end tag as text.

Inside a tag, use attrs(). Inside <script>, <style> or a comment, use raw() — and read data in a script block before putting JSON there. For untrusted comment text, comment() escapes it safely.

This is also the code a frame script or style entry throws when its body is a plain string rather than Html, and the one wrap() throws for such an item in <script> or <style>: escaped text there still runs.

Code 7 ​

A value with no sensible string form.

ts
html`<p>${{ a: 1 }}</p>`; // ✗ object
html`<p>${fetchUser()}</p>`; // ✗ Promise

Await before building the template. For an object, pass the intended property. TypeScript reports this first: the parameter type is Renderable, so neither one typechecks.

Code 8 ​

A tag that never reaches its >.

ts
html`<div class="a" <p>`; // ✗
What the browser does

Reads the <p as an attribute name on the div, producing one element with an attribute called <p and no paragraph at all.

Code 9 ​

An element still open when the template ends, or one that the next start tag closed implicitly.

ts
html`<div><p>x</p>`; // ✗ the div is never closed
html`<ul><li>a<li>b</ul>`; // ✗ the second <li> closed the first
What the browser does

An unclosed element swallows whatever follows it — in a list of components, the next sibling ends up inside the previous one. HTML does permit omitting </li>, </p> and some others, but in a template the likelier reading is that the end tag was forgotten.

The browser closes more than the spec's list of omittable end tags: a <p> at any block such as <xmp> or <listing>, an <option> at an <hr>, a table cell, row or section at any table part that cannot sit in it. Each is reported where the parser does it.

To open in one template and close in another, say so with raw():

ts
const open = html`${raw('<main class="page">')}<h1>${title}</h1>`;
const close = html`${raw('</main>')}`;

Code 10 ​

An end tag closing the wrong element, or nothing at all.

ts
html`<b><i>x</b>`; // ✗
html`</p>`; // ✗ closes nothing
What the browser does

Closes the wrong element, then re-opens what it had to close. A stray </p> makes the parser insert an empty <p></p>, which is why unexplained empty paragraphs turn up in rendered pages.

Code 11 ​

/> on an element that does not self-close.

ts
html`<div />`; // ✗
html`<my-element />`; // ✗ custom elements do not self-close either
html`<br />`; // fine: void element
What the browser does

Ignores the slash, opens the element, and never closes it. Everything after it ends up inside.

/> does self-close inside <svg> and <math>, and the check follows that.

A formatter is a common cause — Prettier and oxfmt rewrite <br> to <br /> inside templates unless embedded formatting is turned off.

Code 12 ​

An end tag for an element that never has one.

ts
html`</br>`; // ✗
html`<img src="${url}"></img>`; // ✗
What the browser does

</br> inserts a second <br>. Other void end tags are ignored, so the markup is merely wrong rather than harmful.

Code 13 ​

Nesting the parser refuses to keep.

ts
html`<p><div>x</div></p>`; // ✗
html`<a href="${x}"><a href="${y}">…</a></a>`; // ✗
html`<table><tr><td>x</td></tr></table>`; // ✗ no tbody
html`<table> total: <tr>…</tr></table>`; // ✗ text directly in a table
html`<svg><p>x</p></svg>`; // ✗ an HTML tag that ends the SVG
html`<body>…</body><script src="a.js"></script>`; // ✗ after </body>
html`<div><tr><td>x</td></tr></div>`; // ✗ table parts outside a table
html`<table><svg>…</svg></table>`; // ✗ anything but a table part, in a table
html`<body><body class="x">…</body></body>`; // ✗ a second <body>
What the browser does

Rewrites it. A <div> inside a <p> closes the paragraph first, leaving an empty <p></p> before the div and a stray </p> after it. A nested <a> is moved out. A <tr> with no <tbody> gets one inserted, so a CSS selector or a querySelector written against the source markup misses. Text directly inside a table is moved out in front of it. Inside SVG or MathML, an HTML tag such as <p>, <div> or <img> closes the foreign content and starts over as HTML. Anything after </body> is moved back into the body, and anything that belongs in the head, after </head>, back into the head. A <tr>, <td> or other table part outside a table is dropped, with its text kept. A second <html> or <body> is dropped and its attributes added to the first, and a <head> after the head is dropped.

Code 14 ​

The same attribute written twice on one tag.

ts
html`<a class="a" class="b">…</a>`; // ✗
What the browser does

Keeps the first and discards the second, silently — usually the opposite of what was intended, since the later one is normally the override.

Code 15 ​

An id reference that points at no id on the page. Reported by check() only, since a single template cannot see the whole page.

ts
html`<label for="email">Email</label>`; // ✗ if no element has id="email"

Checked on for, form, list, headers, popovertarget, commandfor, itemref and the aria-* relations. A broken for means clicking the label does nothing and the input has no accessible name.

Pass check(markup, { ids: false }) when checking a fragment whose references point outside it.

Code 16 ​

The same id on two elements. check() only.

ts
html`<div id="main"></div><div id="main"></div>`; // ✗

Every id reference then resolves to the first, and getElementById returns the first.

Code 17 ​

A tag name that is not a legal tag name, where a tag is built from a string.

ts
wrap(names, 'li li'); // ✗
element({ tag: '<script>' }); // ✗

A legal name starts with a letter and continues with letters, digits, - or _. This is the guard on the places that take a tag name as data: wrap(), element() and frame entries.

Code 18 ​

A body on an element that cannot have one.

ts
element({ tag: 'meta', attrs: { charset: 'utf-8' }, body: 'x' }); // ✗

Void elements — meta, link, br, img and the rest — have no end tag, so there is nowhere for a body to go.

Code 19 ​

A URL the guard replaced with about:blank#blocked. Reported by check(), never thrown, because the URL came from data rather than from the template.

ts
check(String(html`<a href="${'javascript:alert(1)'}">x</a>`));
// [{ code: 19, … }]

Finding one means something upstream produced a URL with a scheme outside the allowed set. Either the data is wrong, or the scheme should be allowed — see adding a scheme.

Code 20 ​

A value trusted would write differently from html. Development build only.

ts
trusted`<p>${'Tom & Jerry'}</p>`; // ✗ html writes Tom &amp; Jerry
trusted`<a href="${'javascript:x'}">x</a>`; // ✗ the guard would block it

In production trusted writes every value as it is. The development build refuses any value that html would escape or block, so production never writes something html would not. Use html for a template that takes this value.

MIT licensed.