Skip to content
datahacks.dev
XML

Parsing XML in JavaScript With DOMParser in the Browser

DOMParser reads XML in the browser but reports errors as a parsererror element, not an exception. Query with XPath, handle namespaces, compare with HTML mode.

6 min read
Summarize in AI
Flat illustration of paired angle-bracket tags wrapped around a document, with the XML logo.

Browsers include an XML parser, and it is available to every web page through DOMParser. For a page that fetches an XML feed, reads an SVG, or processes a file the user dropped on it, that is all you need: no library, and an API you already know because the result is a DOM document. It has one behavior that catches almost everyone, which is how it reports errors.

Parse, query, read

JavaScript
const doc = new DOMParser().parseFromString(xml, "application/xml");
for (const b of doc.getElementsByTagName("book")) {
  const n = b.getElementsByTagName("note")[0];
  console.log(b.getAttribute("id"), b.getElementsByTagName("title")[0].textContent,
              n ? "'" + n.textContent + "'" : "<absent>");
}
Text
parsererror: no
b1 | Cache Patterns | Ana Lee; Bo Chen | 29.50 USD | available=true | note='Covers R&D'
b2 | SQL Tuning, 2nd Edition | Cy Ortiz | 44.00 USD | available=false | note=<absent>
b3 | Indexes "Explained" | Di Park | 250000 VND | available=true | note=''

The second argument is the MIME type: application/xml (or text/xml, image/svg+xml) selects the XML parser, and text/html selects the HTML parser. The result is a standard Document, so getElementsByTagName, getAttribute and textContent work as on a web page, and an absent note is undefined while an empty one is a node with empty text, which the output shows. The ampersand in the first note is decoded. Everything is a string: convert 29.50 with Number() for display and with integer arithmetic for sums.

XPath is built in. The evaluate method takes an expression, a context node, a namespace resolver (or null), a result type and a result object:

JavaScript
const r = doc.evaluate("sum(//book[price/@currency='USD']/price)", doc, null, XPathResult.NUMBER_TYPE, null);
console.log(r.numberValue);
Text
XPath sum USD: 73.5

73.5, the same as every other language in this cluster. The XPath is version 1.0, so the usual function list applies (XPath functions worth learning for everyday XML work). querySelector also works on XML documents for plain names: doc.querySelector("book[id=b2] title") returned the title of the second book. CSS selectors do not understand namespace prefixes, so for namespaced content use XPath with a resolver or the ...NS methods.

Errors do not throw

The standard says parseFromString on malformed XML does not raise an exception. It returns a document that represents the error, and the representation differs by browser. In Chromium, I got this for <a><b></a>:

JavaScript
const bad = new DOMParser().parseFromString("<a><b></a>", "application/xml");
const pe = bad.getElementsByTagName("parsererror")[0];
Text
bad doc: root=a | parsererror text: This page contains the following errors:error on line 1 at column 11: Opening and ending tag mismatch: b line ...

The result had a as its root element, and inside it a parsererror element holding the message with the line and column. Other engines place it differently (the specification leaves the form to the browser, and I have only run Chromium here). The portable check is to search the whole document for the element, in any namespace:

JavaScript
function parseXml(text) {
  const doc = new DOMParser().parseFromString(text, "application/xml");
  const err = doc.getElementsByTagName("parsererror")[0];
  if (err) throw new Error(err.textContent.trim());
  return doc;
}

Wrap every parse in a function like this one, so that a truncated download does not become a "valid" document with the parsable part in it. In Chromium I checked that the parsererror element lives in the XHTML namespace and is a child of the root a, so getElementsByTagNameNS("*", "parsererror") found it too and is the more careful search. Remember also that the document you get back on error may still contain the content parsed before the error, so a loop over book elements after a failed parse can return some books. Always check first.

Namespaces

DOM methods that take a name match the qualified name, prefix included, which surprises people who expect local names:

JavaScript
const ns = new DOMParser().parseFromString('<l:library xmlns:l="urn:example:library"><l:book/><l:book/></l:library>', "application/xml");
console.log(ns.getElementsByTagName("book").length, ns.getElementsByTagNameNS("urn:example:library", "book").length);
Text
getElementsByTagName('book'): 0 | NS-aware: 2

getElementsByTagName("book") found nothing because the elements' qualified name is l:book. (getElementsByTagName("l:book") would have matched, which ties the code to the prefix the sender chose.) getElementsByTagNameNS(uri, "book") matches on the namespace URI and the local name, and is the call to use; "*" as the namespace matches any. Atom feeds, SVG with xlink and most real vocabularies need it. For XPath, pass a resolver function or document.createNSResolver and use your own prefix.

Entities, HTML mode and safety

The external entity test document produced an empty text in Chromium, so the browser did not read a file (it has no access to one in any case). Entity handling in browsers is limited by design: they do not fetch external DTDs from XML parsed through DOMParser. That makes DOMParser a safe way to parse XML from the network in page script, with the proviso that the content is still untrusted.

The HTML parser is a different thing, and the contrast is instructive. The same broken string parsed with text/html was repaired without complaint:

Text
html parse of same bad doc: <a><b></b></a>

HTML parsing never fails and always builds a tree, using the browser's error recovery rules, which is the right behavior for web pages and the wrong one for data. If your code sees a feed through text/html, you will get a tree and no error. Pick the MIME type deliberately, and never use innerHTML to parse XML.

For output, new XMLSerializer().serializeToString(node) writes a node back as XML text: the price element came out as <price currency="USD">29.50</price>. It escapes text and attributes, which is the safe way to build XML in a page, and createElementNS plus appendChild build documents without string pasting (sanitizing data you put into an XML document).

Fetching

fetch(url).then(r => r.text()) then parseXml(text) is the usual path. XMLHttpRequest with responseXML parses for you and returns null on error. Remember cross-origin rules (the server must send CORS headers for a feed from another site), and the encoding: response.text() decodes as UTF-8, so a feed in another encoding should go through arrayBuffer() and a TextDecoder with the right label.

For other languages see parsing XML in Java, C# and other typed languages, and for Node parsing XML in Node with fast-xml-parser. The browser API is documented in the MDN DOMParser reference and the XPath interface in MDN: Document.evaluate. Test an expression in the XPath tester.

When DOMParser is not enough in the browser

The file is very large. The parser builds the whole tree in the page's memory. For big files, stream on a server or in a worker with a streaming parser.

You need schema validation or XSLT with modern features. Browsers provide XSLTProcessor for XSLT 1.0 and no schema validation.

You need consistent error text. The parsererror format varies between browsers. Extract the line and column with a pattern per engine, or validate on the server.

You run in Node. There is no DOMParser in Node's standard library. Use a package; the next article covers one.

Also inTooling
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.