HTML escaping is one of those chores that sits quietly in the background until it doesn't. Get it wrong and you either break your page layout or, worse, open the door to script injection. Get it right and nobody ever notices — which is exactly the point.
What escaping actually does
Certain characters have special meaning in HTML. The browser sees < and starts looking for a tag. It sees & and expects an entity reference. If your content contains those characters as literal text, you have to tell the browser to treat them as data, not markup. That's escaping: replacing reserved characters with their entity equivalents.
The core set you care about is small:
&becomes&<becomes<>becomes>"becomes"'becomes'(or')
Escape the ampersand first, always. If you replace < with < after you've already done ampersands, you're fine. Do it the other way and you'll double-escape and end up with &lt; showing up as literal text on the page.
When you need it
Any time untrusted or dynamic content ends up inside HTML. The classic case is user input echoed back to the page — comments, usernames, search terms. If a user types <script>alert(1)</script> into a comment box and you drop that raw into the DOM, congratulations, you've shipped a cross-site scripting vulnerability.
Other common situations:
- Displaying code samples on a page (the whole reason this article's snippets render properly).
- Putting dynamic values into HTML attributes.
- Embedding data from a database or API that might contain angle brackets or ampersands.
- Generating HTML emails or reports server-side.
Escaping is context-dependent
This is the part people miss. HTML-escaping is correct for content between tags. It is not sufficient for every context. Data going into a JavaScript string needs JS escaping. Data going into a URL needs URL encoding. Data inside an unquoted attribute is dangerous even after HTML-escaping, because a space or a slash can break out of the attribute.
Rule of thumb: quote your attributes and escape for the specific place the value lands. Don't assume one escaping pass covers everything.
A worked example
Say you're building a search results page and want to show what the user searched for. Naive server code might do this:
const html = `<p>Results for: ${query}</p>`;If query is <img src=x onerror=alert(1)>, that image tag executes. Here's a minimal escape function:
function escapeHtml(str) {
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
const html = `<p>Results for: ${escapeHtml(query)}</p>`;Now the malicious input renders as harmless text. Note the ampersand replacement comes first — that ordering is not optional.
Common gotchas
Double escaping
The most frequent mistake. You escape a value, then a framework or template engine escapes it again, and users see &lt; on the page. Know what your template engine does by default. Most modern ones — React JSX, Vue, Handlebars with double braces, Jinja2 — auto-escape interpolated values. If yours does, don't escape manually on top of it.
Trusting client-side escaping alone
Escaping in the browser is fine for display, but never rely on it as your only defence. An attacker can bypass your frontend entirely and hit your API directly. Validate and escape on the server where the output is generated.
Forgetting attributes
Content escaping and attribute escaping overlap but aren't identical. Inside an attribute, double quotes matter enormously. Always quote attributes and escape the quote character that closes them.
innerHTML vs textContent
In vanilla JS, the simplest safe path for displaying user text is often to skip escaping altogether and use textContent:
element.textContent = userInput; // browser handles escaping for youUse innerHTML only when you genuinely need to render markup, and only with content you've explicitly escaped or sanitised.
Unescaping when you shouldn't
If you store already-escaped content in your database, then escape it again on output, you get double escaping. Decide once whether you store raw or escaped, be consistent, and escape at the point of output rather than input. Storing raw and escaping on render is usually the cleaner model.
Doing it quickly
When you're debugging a template, pasting a code sample into docs, or just need to sanity-check what a string looks like escaped, hand-rolling a function is overkill. I keep a browser tab open on the HTML Escape Tool for exactly these one-off conversions — paste in your text, get the escaped output, done. It handles the reverse direction too, which is handy when you're trying to read some heavily-encoded string from a log.
The short version
- Escape reserved characters so the browser treats content as data, not markup.
- Escape the ampersand first to avoid double encoding.
- Escape on output, escape for the specific context, and quote your attributes.
- Let auto-escaping template engines do their job — don't stack manual escaping on top.
- Never treat client-side escaping as your security boundary; do it server-side too.
HTML escaping isn't glamorous, but it's the difference between a page that renders as intended and one that either looks broken or runs somebody else's JavaScript. Build the habit of thinking about where every dynamic value lands, and it becomes second nature.
Photo by Florian Olivo on Unsplash