Escaping HTML entities is one of those things you learn early, forget about, and then rediscover the hard way when user input breaks your page or opens an XSS hole. It's simple in concept but surprisingly easy to get wrong, especially once you factor in attributes, JavaScript contexts, and frameworks that do half the work for you.

Here's the short version of what's actually going on and how to not shoot yourself in the foot.

What escaping HTML entities actually means

Certain characters have special meaning in HTML. The browser sees < and starts looking for a tag. It sees & and expects an entity reference. If those characters show up in your data — a username, a comment, a product description — and you drop them straight into markup, the browser interprets them as code instead of text.

Escaping replaces those characters with their entity equivalents so the browser renders them literally:

  • & becomes &amp;
  • < becomes &lt;
  • > becomes &gt;
  • " becomes &quot;
  • ' becomes &#39; (or &apos;)

Those five are the core set. Get them right and you've covered the vast majority of cases.

Why you'd bother

Two reasons, and they overlap.

1. Correctness

Say a user writes a comment: I love 5 < 10 & that's a fact. Without escaping, the browser reads < 10 as the start of a tag and swallows part of the text. Your page displays garbage. Escaping keeps the text as text.

2. Security

This is the big one. Cross-site scripting (XSS) happens when attacker-controlled input gets interpreted as HTML or JavaScript. If someone submits <script>steal(document.cookie)</script> and you render it unescaped, that script runs in every visitor's browser. Escaping neutralises it — the script tag becomes visible text instead of executable code.

The rule of thumb: escape at the point of output, based on the context you're outputting into. Not at input. Storing pre-escaped data leads to double-escaping and endless headaches.

Context matters more than people think

The five-character set works for HTML body content. But HTML isn't one context — it's several, and each has different escaping rules.

  • Element content (<p>here</p>) — the standard entity escaping.
  • Attribute values — you must escape quotes, and always quote your attributes. An unescaped quote breaks out of the attribute.
  • Inside <script> tags — HTML escaping does not help here. You need JavaScript string escaping.
  • URLs — use URL encoding, not HTML escaping.

A classic mistake is HTML-escaping a value and then dropping it into a JavaScript block, assuming you're safe. You're not. Different context, different rules.

Common gotchas

Double escaping

Escape something twice and &amp; turns into &amp;amp;. Users then see literal &amp; on the page. This usually happens when one layer escapes on save and another escapes on render. Pick one place — output — and stick to it.

Escaping the wrong thing

Escaping trusted, static text you wrote yourself is harmless but pointless. The danger is forgetting to escape dynamic values. Assume every piece of data that isn't a hardcoded literal needs escaping until proven otherwise.

Unquoted attributes

If you write <input value=$userInput> without quotes, escaping quotes isn't enough — a space in the input creates a new attribute. Always quote attribute values.

Trusting innerHTML

Setting element.innerHTML = userInput parses the string as HTML. Use textContent instead when you just want text — the browser escapes automatically and you sidestep the whole problem.

A worked example

Here's a tiny escape function in JavaScript. It's the kind of thing you'll find inside most templating libraries:

function escapeHtml(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

const comment = `<script>alert('xss')</script> & more`;
document.getElementById('out').innerHTML = escapeHtml(comment);
// Renders as visible text:
// <script>alert('xss')</script> & more

Notice the order: & is escaped first. If you escaped < to &lt; and then ran the ampersand replacement, you'd turn that & into &amp;lt; and break everything. Order is not optional.

For a quick sanity check — pasting in a chunk of user input, a JSON blob, or some markup you want to display as-is in docs — I keep a browser tab open with the HTML Escape Tool. It's faster than writing a throwaway script when you just need to escape or unescape a string once and move on.

In practice: let your framework help

Modern frameworks escape by default, which is exactly right. React escapes anything you put in JSX expressions. Vue escapes mustache interpolation. Server-side templating engines like Jinja, ERB, and Blade auto-escape output. The moment you reach for the escape hatch — dangerouslySetInnerHTML, v-html, raw, {!! !!} — you're taking responsibility yourself. That's when bugs and vulnerabilities creep in.

So the practical advice:

  • Lean on auto-escaping. Don't disable it unless you truly need raw HTML.
  • When you do render raw HTML from user input, sanitise it with a proper library (like DOMPurify) rather than hand-rolling a filter.
  • Escape based on context — body, attribute, script, URL are all different.
  • Escape once, at output. Never on save.

Escaping isn't glamorous, but it's the difference between a page that shows a user's <3 correctly and a page that runs a stranger's JavaScript in your customers' browsers. Learn the five characters, respect the context, and let your tooling do the repetitive part.

Photo by Ferenc Almasi on Unsplash