Seeing & or ' rendered as visible text on a page is one of the most recognisable display bugs on the web. The content is not corrupt; it has been encoded once too often, and somewhere in the pipeline a decoding step is missing or a value was escaped twice.
Fixing it properly means finding where the extra encoding happened rather than adding a decode call at the end. This guide covers the correct decoding approach in each environment and, more importantly, how to tell which layer introduced the problem.
Why entities appear as literal text
An HTML entity is markup instructing the parser to produce a character. When a browser parses & it renders an ampersand. If you see the entity itself on screen, the text was escaped twice: the ampersand of the entity was itself encoded, so & became & and then &.
The usual cause is a value escaped in application code and then escaped again by a template engine that escapes output by default. Modern templates in Twig, Blade, React, and Vue all escape automatically, so any manual escaping before them is duplication rather than defence.
Decode HTML entities JavaScript offers in the browser
The reliable HTML entity decoder in a browser is DOMParser, which parses a string with the real HTML parser and returns the resulting text. Parsing the string and reading documentElement.textContent handles named entities, numeric references, and edge cases exactly as the browser does.
The widely shared alternative assigns the string to a temporary element's innerHTML and reads back textContent. It works, but it parses arbitrary markup, so an input containing <img src=x onerror=…> can execute script. Never use it on untrusted input, and prefer DOMParser in every case.
Decoding in Node
Node has no DOM, so the browser techniques are unavailable. Use a maintained library such as he or entities, which implement the full named entity table and the numeric reference rules including the legacy Windows-1252 remapping the specification requires.
Avoid a hand-written replacement chain over the five common entities. It covers the obvious cases and silently fails on everything else, including numeric references, hexadecimal forms, and unterminated entities that browsers still decode. The entity table has well over two thousand names, and reimplementing it is not worthwhile.
html_entity_decode PHP flags and defaults
PHP provides html_entity_decode() for all entities and htmlspecialchars_decode() for only the five that htmlspecialchars() produces. Passing the flags and character set explicitly is essential: use ENT_QUOTES | ENT_HTML5 and 'UTF-8'.
When html_entity_decode() appears not to work, the cause is almost always defaults. Without ENT_QUOTES it leaves single-quote entities alone, and with a mismatched character set it leaves non-ASCII entities untouched. Check the flags before concluding the input is unusual.
React and modern frameworks
React escapes everything rendered as a child, so a string containing entities displays them literally. That is correct behaviour and the reason React is resistant to injection by default. Decoding the string before rendering is the right fix.
The escape hatch, dangerouslySetInnerHTML, is named as a warning. Use it only for markup you generated or sanitised yourself, never for user-supplied content. If the goal is simply to show an ampersand or a quotation mark, decode to a plain string and render it normally.
Fixing the cause, not the symptom
Adding a decode step at the point of display often hides the real defect and creates a new one. If a value can arrive already decoded, decoding again transforms legitimate text: a user who literally types & in a comment will see it become an ampersand, and repeated round trips progressively corrupt the content.
Trace the value backwards instead. Store text unescaped in the database, escape exactly once at the moment of rendering, and let the template engine own that step. A value that is escaped when it is stored will be escaped again on output by any modern template, which is precisely how the double encoding arises.
Entities and character encoding are different problems
Entities produce visible & sequences. A character encoding mismatch produces mojibake, where an accented letter appears as two or three unrelated symbols. They look similar to a user and have entirely different causes.
Mojibake means UTF-8 bytes were interpreted as a single-byte character set, and no amount of entity decoding will repair it. Fix it by serving Content-Type: text/html; charset=utf-8, declaring the character set in a meta tag, and confirming the database connection and column collation both use UTF-8.
Decode HTML entities in every environment
The question of how to decode HTML entities in JavaScript, PHP, or Node has a different answer in each, and the wrong one either misses cases or opens an injection path.
| Environment | Call | Caveat |
|---|---|---|
| Decode HTML entities JS, in the browser | new DOMParser().parseFromString(s, "text/html").documentElement.textContent | Safe for untrusted input |
| JavaScript decode HTML entities in string, quick form | el.innerHTML = s; el.textContent | Executes markup; never use on untrusted text |
| Decode HTML entities Node | decode(s) from html-entities | No DOM available; a library is required |
| Decode HTML entities PHP | html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8') | Defaults are the usual cause of failure |
| C# decode HTML entities | WebUtility.HtmlDecode(s) | HttpUtility is the older equivalent |
| Decode HTML entities React | Decode to a string, then render it | dangerouslySetInnerHTML only for trusted markup |
| html_entity_decode Twig | {{ s|raw }} after decoding upstream | Twig escapes on output; decode before, not after |
The names differ by ecosystem but the operation does not. An html_entity_decode in PHP call, an html_entity_decode in JavaScript equivalent, an html entity decode javascript helper and a decode HTML entities nodejs library all resolve the same character references. Likewise, decode HTML escape characters, convert HTML escape characters to text, decode HTML special characters JavaScript, and convert HTML character entities to UTF 8 all describe this single step.
When html_entity_decode not working is the symptom, the answer is almost never the input. Check the three arguments in order: without ENT_QUOTES the function leaves single-quote entities untouched, without ENT_HTML5 it misses names added after HTML4, and with the wrong character set it silently skips everything outside ASCII. An html_entity_decode PHP online sandbox is a quick way to confirm which flag is missing before changing application code.
A related need is to convert HTML character entities to UTF-8 text for storage or search. That is the same decode step followed by writing the result as UTF-8; there is no separate conversion. Decoding HTML escape characters and decoding HTML special characters in JavaScript are the same operation under different names, and all of them are served by the DOMParser approach above.
A decoding checklist
Identify what you are looking at first: a visible entity means double escaping, and garbled accents mean an encoding mismatch. Then locate the layer that escaped the value, rather than compensating downstream.
When inspecting an unfamiliar string, paste it into an HTML entity decoder to see what it resolves to. Counting the layers of & tells you exactly how many times the value was escaped, which usually points straight at the code responsible.
References: the WHATWG named character reference table is the authoritative entity list, and the OWASP XSS Prevention Cheat Sheet covers context-aware escaping.