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.
| code | meaning | from |
|---|---|---|
| 2 | an object or array for an attribute other than class, style, aria, data | attrs |
| 3 | a refused attribute: anything starting with on | html, attrs |
| 5 | an unquoted attribute value before an expression | html |
| 6 | a non-Html expression inside a tag, <script>, <style> or a comment | html, frame |
| 7 | a value that cannot be rendered: an object, a Promise, a symbol | html |
| 8 | a tag never closed with > | html |
| 9 | an element still open at the end, or one the next start tag closed implicitly | html |
| 10 | an end tag that closes nothing, or the wrong element | html |
| 11 | /> on an element that does not self-close | html |
| 12 | an end tag on a void element | html |
| 13 | nesting the browser rewrites | html |
| 14 | the same attribute twice on one tag | html |
| 15 | an id reference with no matching id | check |
| 16 | an id used twice | check |
| 17 | a bad tag name | frame, element, wrap |
| 18 | a body on a void element | frame, element |
| 19 | a URL the guard blocked | check |
| 20 | a value trusted would write differently from html | trusted |
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.
attrs({ title: { a: 1 } }); // ✗
attrs({ class: ['a', 'b'], style: { color: 'red' } }); // fineFour 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.
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.
html`<a href=${url}>…</a>`; // ✗
html`<a href="${url}">…</a>`; // fineAn 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.
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 goesRight 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 < 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.
html`<p>${{ a: 1 }}</p>`; // ✗ object
html`<p>${fetchUser()}</p>`; // ✗ PromiseAwait 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 >.
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.
html`<div><p>x</p>`; // ✗ the div is never closed
html`<ul><li>a<li>b</ul>`; // ✗ the second <li> closed the firstWhat 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():
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.
html`<b><i>x</b>`; // ✗
html`</p>`; // ✗ closes nothingWhat 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.
html`<div />`; // ✗
html`<my-element />`; // ✗ custom elements do not self-close either
html`<br />`; // fine: void elementWhat 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.
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.
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.
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.
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.
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.
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.
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.
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.
trusted`<p>${'Tom & Jerry'}</p>`; // ✗ html writes Tom & Jerry
trusted`<a href="${'javascript:x'}">x</a>`; // ✗ the guard would block itIn 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.