sax-js is a streaming XML parser that is explicit about what it is not
A sax style parser for JS
At a glance
- What is it?
- sax-js is a sax-style streaming XML and HTML parser by Isaac Z. Schlueter, small enough to read in an afternoon, that emits events instead of building a tree. It fits a Node developer handling RSS, XML or mostly-ok documents, and it does not fit a task that needs a DOM, XSLT, DTD resolution or a real HTML parser.
- Who is it for?
- sax-js fits a Node developer who needs to walk an XML or RSS document event by event, or pipe a file through a parser and write the result out, and it does not fit a task that needs a queryable DOM, XSD validation, DTD entity resolution or correct handling of real-world HTML.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 93 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The README spends more space on what this is not
sax-js is a sax-style parser for XML and HTML, designed with node in mind but said to work fine in the browser or other CommonJS implementations. The package description calls it an evented streaming XML parser in JavaScript, and the author is Isaac Z. Schlueter, at version 1.6.1.
What makes the README memorable is the section titled What This Is (probably) Not. It says explicitly that it is not an HTML parser, that it is just XML. It is not a DOM builder, though you can use it to build an object model out of XML. It is not XSLT, because no DOM means no querying. It is not 100 percent compliant with some other SAX implementation, since most SAX implementations are in Java and do a lot more than this does. It is not an XML validator, doing a little validation in strict mode but not much. It is not a schema-aware XSD thing, with the comment that schemas are an exercise in fetishistic masochism. And it is not a DTD-aware thing, because fetching DTDs is a much bigger job.
That list is not false modesty, it is a scope statement. A parser that claims to do everything is a parser you have to audit before you trust it. sax-js tells you exactly which guarantees you are getting, which for a parser handling input you did not write is most of the value.
The positive list is short: a very simple tool to parse through an XML string, a stepping stone to a streaming HTML parser, and a handy way to deal with RSS and other mostly-ok-but-kinda-broken XML documents. The last of those is the honest description of the target workload.
Events, not a tree, and the event surface is documented
The API is a parser object with handler properties, and the README documents the events in detail.
The basic shape is a write and a close, with handlers assigned to on<eventname> properties. Functions execute in the this-context of the parser object, and the list of supported events is also exported in an EVENTS array.
The events include error, which carries an instance of Error and must be cleared from parser.error before parsing continues, and which happens much more in strict mode. There is text for a text node, with the string as argument. There is doctype for the DOCTYPE declaration. There is processinginstruction for things like an XML declaration, with an object carrying name and body members, and attributes are not parsed because processing instructions have implementation dependent semantics. There is sgmldeclaration for random SGML declarations such as an ENTITY declaration, and the README notes this is a weird thing to support and might go away at some point, since SAX was not intended for SGML.
The tag events are the ones you will use most. opentagstart fires immediately when the tag name is available, before any attributes, with a name field and an empty attributes set, and it is the same object later emitted in the opentag event. opentag carries an object with name and attributes, with tag names uppercased in non-strict mode unless the lowercase option is set, and namespace binding information on the ns member plus local, prefix and uri members if the xmlns option is set. closetag carries the tag name, and in loose mode tags are auto-closed if their parent closes, while in strict mode well-formedness is enforced, and self-closing tags emit closetag immediately after opentag. There is also an attribute event with name and value.
The opentagstart split is the one that pays for itself. It lets you act on the tag name before you have parsed its attributes, which matters when the attribute values are large or when you want to decide early whether you care about the subtree at all.
Strict and loose mode are two parsers, not one parser with a tolerance knob
The first argument to the parser function is strict, a boolean, and the README describes it as whether or not to be a jerk, defaulting to false. The usage example sets strict to true with a comment saying to set it to false for html-mode.
The distinction is larger than the name suggests. In loose mode, tag names are uppercased unless the lowercase option is set, tags are auto-closed if their parent closes, unquoted attribute values are allowed by default, and unknown entities pass through unmolested. In strict mode, well-formedness is enforced, unquoted attribute values are rejected, and unknown entities fail.
That last point is the one that bites. A document with an undefined entity, say , is invalid XML but is common in the wild, and in strict mode the parser will stop on it. In loose mode it passes through as literal text. If your input is machine-generated and valid, use strict. If your input came from a third party and you want to survive it, use loose and accept the loss of guarantees.
The second argument is opt, an object bag of string formatting settings, all defaulting to false. trim decides whether text and comment nodes are trimmed. normalize turns any whitespace into a single space. lowercase lowercases tag and attribute names in loose mode rather than uppercasing them. xmlns enables namespace support. position, when false, stops the parser tracking line, column and position, which is a real cost when you are parsing large documents and do not need to report where an error occurred. strictEntities limits parsing to the five predefined XML entities, and unquotedAttributeValues allows unquoted attribute values, defaulting to false when strict is true and true otherwise.
The stream interface is the part that scales
The stream usage is documented next to the direct usage, and it is where the library earns its place for large documents.
createStream(strict, options) takes the same options as the parser and returns a proper node event emitter, so handlers are attached with the normal on function. Unhandled errors will throw, since it is a proper node event emitter. The README shows the error handling pattern, which involves logging, clearing the error by setting this._parser.error to null, and calling this._parser.resume.
That last part is the key to the error model. The parser does not continue while in an error state, so a handler that wants to continue must clear the error and resume. This is deliberate: a parser that silently continued after a syntax error would hand you a partial document you might mistake for a complete one.
The pipe example shows the whole shape, and it is the most useful snippet in the README:
fs.createReadStream('file.xml')
.pipe(saxStream)
.pipe(fs.createWriteStream('file-copy.xml'))The same chunks coming in also go out, so a saxStream is both readable and writable. That means the canonical use of this library is a transform in a stream pipeline: read a file, parse it as it arrives, and write the result out, all without the document ever being fully in memory.
The methods mirror the same model. write puts bytes onto the stream and you do not have to do it all at once, you can keep writing as much as you want. close closes the stream, and once closed no more data may be written until it is done processing the buffer, which is signalled by the end event. resume is the graceful error recovery described above.
Parser state is exposed, which is how you report an error location
The parser object always has a documented set of members, and the README is candid that there are others you probably should not touch.
The public ones are the position members. line, column and position indicate the position in the XML document where the parser currently is. startTagPosition indicates the position where the current tag starts, which is the one you want in an error message, because the line where the parser gave up is rarely the line where the problem is. closed is a boolean saying whether the parser can be written to, and if it is true you wait for the ready event to write again. strict reflects the mode you constructed it with. opt holds the options you passed in. tag is the current tag being dealt with.
The position tracking is controlled by the position option, and the README makes the trade explicit: if false, then don't track line, column and position. That is a performance and memory trade for a parser whose whole point is to handle large streams cheaply.
The distinction between line, column and position as three separate members is worth noting. Column and line are human-facing, position is the offset in the stream, and a tool that reports byte offsets alongside line and column is one you can use to seek in the source file.
The entity handling is the other part of the state model, and it is where the README is most explicit about what the parser refuses to do. It handles the basic XML entities in text nodes and attribute values, namely the ampersand, less-than, greater-than, apostrophe and quote entities. It is possible to define additional entities in XML by putting them in the DTD, and this parser does not do anything with that. The README even suggests the workaround: if you want to listen to the ondoctype event, then fetch the doctypes, and read the entities and add them to parser.ENTITIES, then be my guest. The ENTITIES member is the extension point, and filling it is left to the caller.
Where sax-js is the wrong tool
The first wrong-tool case is real HTML. The README says so directly: an HTML parser is a fine goal, but this is not it, it is just XML. The loose mode relaxes the parser enough to survive some broken markup, but that is not the same as understanding HTML. A document with implicit tag closing, misnested inline elements, entity references that are not XML entities, and content models that do not exist will produce something the loose mode can survive and your code cannot use. If you need to parse HTML, use an HTML parser.
The second is any task that needs to query the document. There is no DOM, so there is no XPath, no CSS selector, and no random access. Building an object model out of the events is possible, the README says so, but it does not do it out of the box, and you end up writing the tree builder yourself.
The third is entity-heavy XML. If your documents use custom entities defined in a DTD, the parser will not resolve them and will not fetch the DTD. You have to listen for the doctype event, fetch the DTD yourself, parse the entity declarations and populate parser.ENTITIES. That is a real project for anyone whose XML uses the older internal DTD subset, and one where a parser with proper DTD support would simply work.
The fourth is validation and schema. Strict mode does some well-formedness checking, but the README is clear it is not a validator, and XSD support does not exist. If you need to enforce a schema, you need a different tool.
The last consideration is the shape of the library itself, and it is worth stating plainly because it is a genuine trade. The published package ships a single source file. The files array in package.json lists lib/sax.js, LICENSE and README.md, and there is no build step and no generated directory. You can read the whole parser in one sitting, which is a serious advantage when you are deciding whether to trust it, and a disadvantage when you are looking for the features it deliberately does not have.
Editorial conclusion
sax-js fits a Node developer who needs to walk an XML or RSS document event by event, or pipe a file through a parser and write the result out, and it does not fit a task that needs a queryable DOM, XSD validation, DTD entity resolution or correct handling of real-world HTML. Before adopting it, decide whether you want strict or loose mode, because the difference is not a tolerance setting but a different parser, and note that the strictEntities option restricts you to the five predefined XML entities if your documents rely on anything else.
Frequently asked questions
What is sax-js and what does it do?
sax-js is a sax-style streaming XML parser for JavaScript, designed with node in mind. You create a parser with sax.parser(strict), assign handlers such as ontext, onopentag and onerror, and feed it data with write and close. It emits events rather than building a DOM.
What is the difference between strict and loose mode in sax-js?
In strict mode well-formedness is enforced, unquoted attribute values are rejected and unknown entities fail. In loose mode tags are auto-closed if their parent closes, tag names are uppercased unless the lowercase option is set, unquoted attribute values are allowed, and unknown entities pass through unmolested.
How do I use sax-js as a stream?
Call sax.createStream(strict, options) and attach handlers with the normal EventEmitter on function. The stream is readable and writable, so you can pipe it: fs.createReadStream('file.xml').pipe(saxStream).pipe(fs.createWriteStream('file-copy.xml')). Unhandled errors throw, and to recover you clear this._parser.error and call this._parser.resume().
Does sax-js handle DTDs and custom entities?
No. It handles the five predefined XML entities and does nothing with entities defined in a DTD. The README suggests listening for the doctype event, fetching the doctypes, reading the entities and adding them to parser.ENTITIES. Unknown entities fail in strict mode and pass through unmolested in loose mode.
Can I use sax-js to build a DOM or query the document?
The README says you can use it to build an object model out of XML but it does not do that out of the box, and it is not XSLT because no DOM means no querying. For a queryable document you need a different library.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/isaacs-sax-js)Was this useful?