Escaping HTML entities sounds like a solved problem until you're staring at a page that rendered a user's comment as an actual <script> tag. It's a small mechanic that carries a lot of weight: it's the difference between text being displayed and text being interpreted as markup.
What escaping actually means
HTML has a handful of characters that have special meaning to the parser. When the browser sees < it starts looking for a tag. When it sees & it starts looking for an entity. If you want those characters to appear literally on the page, you have to replace them with their entity equivalents so the parser treats them as plain text.
The core set most people care about:
&becomes&<becomes<>becomes>"becomes"'becomes'(or', but stick with the numeric form for broad compatibility)
Order matters. Always escape the ampersand first. If you escape < to < and then run an ampersand pass, you'll double-escape it into &lt; and end up displaying the raw entity text instead of the character.
Why and when you'd use it
The single most important reason is security. Any time you take input from a user — or from a database, an API, a filename, anything you don't fully control — and drop it into HTML, you have to escape it. Skip this and you've opened the door to cross-site scripting (XSS), where an attacker injects markup that runs in your users' browsers.
Consider a comment form. A visitor types:
<img src=x onerror="alert(document.cookie)">If you write that straight into the page, the browser executes the onerror handler. Escape it, and the visitor just sees the literal text they typed. Same input, wildly different outcome.
Beyond security, escaping is what lets you display code, markup examples, or any text that happens to contain angle brackets. This very article escapes its own examples so you can read them instead of accidentally running them.
Where escaping belongs
Escaping is context-sensitive. HTML entity escaping is for content that lands in the HTML body or inside a regular attribute value. It is not the right tool everywhere:
- Inside a
<script>block, you need JavaScript string escaping, not HTML escaping. - Inside a URL, you need URL encoding.
- Inside a CSS value, you need CSS escaping.
Reaching for the wrong encoder is a classic mistake. HTML-escaping a value and then jamming it into a JavaScript context can still be exploitable.
Common gotchas
Double escaping
This is the one that bites everyone. Data gets escaped when it's saved, then escaped again when it's rendered, and users see & littered across the page. Pick one layer to own escaping — almost always the rendering/output layer — and never store pre-escaped HTML in your database. Store the raw text; escape at the last possible moment.
Trusting your template engine blindly
Most modern template engines auto-escape by default, which is great. But they also give you an escape hatch — {{{ }}} in Handlebars, | safe in Jinja, dangerouslySetInnerHTML in React. The moment you use one of those, you've taken on the escaping responsibility yourself. Audit those spots hard.
Attribute contexts and quotes
If you build unquoted attributes, escaping the standard set isn't enough — a space or other character can break you out of the attribute. Always quote your attributes and escape the quote character you're using. Consistent double quotes plus escaping " covers the common case.
Forgetting the ampersand
People remember angle brackets and forget &. A URL like page?a=1&b=2 written raw into an href is technically malformed and can misbehave. Escape the ampersand to &.
A short worked example
Here's a minimal, correct escaper in JavaScript. Note the ampersand-first ordering:
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
const input = '<img src=x onerror="alert(1)">';
document.getElementById('output').innerHTML = escapeHtml(input);
// renders as literal text: <img src=x onerror="alert(1)">In browser code you can often skip the manual function entirely and let the DOM do it for you:
const el = document.getElementById('output');
el.textContent = userInput; // safe by designUsing textContent instead of innerHTML means the browser never parses the value as markup, so there's nothing to escape. When you have the option, prefer that.
For quick one-off jobs — cleaning up a snippet before pasting it into a blog post, sanity-checking what a string will look like escaped, or debugging why a payload rendered wrong — a browser tool beats writing throwaway code. The HTML Escape Tool handles it in a couple of seconds and unescapes too, which is handy when you're reverse-engineering someone else's output.
Rules of thumb worth remembering
- Store raw, escape on output. Never the other way round.
- Escape the ampersand before anything else.
- Match the encoder to the context: HTML, JS, URL, and CSS are all different.
- Prefer
textContentoverinnerHTMLwhen you're just showing text. - Treat every auto-escape escape hatch as a security review item.
Escaping isn't glamorous, but getting it right removes a whole category of bugs and vulnerabilities. Build the habit of escaping at the boundary where data becomes HTML, keep your stored data clean, and lean on your framework's defaults instead of hand-rolling markup with string concatenation. Do that consistently and you'll rarely think about it again — which is exactly the point.
Photo by Ilya Pavlov on Unsplash