HTML escaping is one of those tasks that feels trivial until it bites you. A stray < in user input breaks your layout. A missed character becomes an XSS hole. Most frameworks handle it for you, right up until you reach the one spot where they don't.
Here's what escaping actually does, when you need it, and the mistakes I keep seeing in code review.
What HTML escaping actually is
Certain characters have special meaning in HTML. The browser's parser treats < as the start of a tag and & as the start of an entity reference. If you want those characters to appear as literal text rather than be interpreted as markup, you replace them with named or numeric entities.
The core set you care about is small:
&becomes&<becomes<>becomes>"becomes"'becomes'(or')
Escape the ampersand first. If you do it last, you'll double-escape the entities you just created and end up with literal &lt; text on the page.
When and why you'd use it
The honest answer: any time untrusted or dynamic text ends up in an HTML document. That covers more cases than people expect.
Preventing cross-site scripting
This is the big one. If a user types <script>alert(1)</script> into a comment box and you render it raw, the browser runs it. Escape the output and it shows up harmlessly as text. Escaping on output is your primary defence against reflected and stored XSS.
Displaying code and markup as text
Documentation sites, tutorials, and anything that shows HTML snippets need escaping so the examples render as text instead of being parsed. If you're writing a blog about HTML, you escape every example you don't want the browser to execute.
Fixing broken rendering
Even without a security angle, unescaped characters cause visual bugs. A product description containing Width < 10cm will swallow everything after the < as a malformed tag. Escaping keeps the text intact.
Context matters more than the character set
The single most important thing to understand: HTML escaping is context-dependent. The rules for escaping text between tags are not the same as the rules for an attribute value, a URL, or inline JavaScript.
Consider this template:
<a href="/search?q=USER_INPUT" title="USER_INPUT">USER_INPUT</a>That input appears in three different contexts, each needing different treatment:
- Inside the
titleattribute, you need HTML attribute escaping (at minimum the quote characters). - Inside the
hrefURL, you need URL encoding, not HTML escaping. - Between the tags, standard HTML text escaping applies.
Applying plain HTML escaping everywhere is better than nothing, but it won't save you inside an unquoted attribute or a <script> block. For those, use a context-aware templating engine.
Common gotchas
Double escaping
This happens when data passes through two escaping layers. You escape it in the backend, then your template engine escapes it again, and users see &lt; on screen. The fix is to decide on one layer that owns escaping and make sure the others pass data through raw. Don't escape-on-store and escape-on-render both.
Trusting auto-escaping blindly
Modern frameworks auto-escape by default in HTML text context. That's great. But the moment you use a "raw" or "safe" helper, you're on your own. Every dangerouslySetInnerHTML, v-html, or {{{ }}} is a spot where escaping is disabled and you have to think hard.
Attribute values without quotes
If you write <div class=VALUE> without quotes, an attacker can inject an event handler like onmouseover=... without ever needing an angle bracket. Always quote attribute values, then attribute escaping becomes straightforward.
Escaping when you shouldn't
Escaping data destined for a database, a JSON API, or a log file is a mistake. HTML escaping is for HTML output only. If you escape on input, your stored data gets corrupted and every non-HTML consumer sees entity gibberish. Store raw, escape at the point of rendering.
A short worked example
Here's a minimal, dependency-free escape function in JavaScript for the HTML text context:
function escapeHtml(str) {
return String(str)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
const comment = '<img src=x onerror="alert(1)">';
document.getElementById('out').textContent = comment; // safest: textContent
document.getElementById('out').innerHTML = escapeHtml(comment); // if you must use innerHTMLNotice the order: the ampersand replacement runs first. Run it last and the & characters inside your new entities get re-escaped.
Also notice the better option sitting right there. If you're just inserting text, textContent (or innerText) does the escaping for you and sidesteps the whole problem. Reach for manual escaping only when you're building an HTML string by hand.
When you just need to convert a chunk of markup or a snippet quickly without wiring up code, our HTML Escape Tool handles the conversion both ways in the browser. It's handy for prepping code samples or sanity-checking what an entity actually resolves to.
Quick checklist
- Escape on output, never on input or storage.
- Escape the ampersand before anything else.
- Match the escaping to the context: text, attribute, URL, and script are all different.
- Always quote your HTML attributes.
- Treat every "raw HTML" helper as a code smell that needs a second look.
- Prefer
textContentover building HTML strings when you only need text.
Escaping isn't glamorous, but getting it right is the difference between a comment box and a security incident. Lean on your framework's auto-escaping where you can, understand exactly where it stops, and keep the data flow clean: raw in storage, escaped at the edge.
Photo by Danial Igdery on Unsplash