Why a Brace-Counting JS Formatter Breaks for Loops
Most free JS formatters that run instantly in the browser don't actually "understand" code. A real tool like Prettier parses code into a syntax tree (AST) and re-emits from that tree, but a lightweight formatter just scans characters one at a time and decides where to break lines based on a handful of symbols. Here's why that difference exists and where it causes problems, verified against the actual code.
1. Why character counting instead of an AST parser
Building a full JS parser from scratch — tokenizing, parsing grammar, handling ASI (automatic semicolon insertion) rules — takes hundreds to thousands of lines. By contrast, the rule "increase indentation when a brace opens, decrease it when one closes" takes just a few lines and satisfies most of what people actually want: turning minified code into something readable. The JavaScript Beautifier takes this same approach — open the actual code and you'll find no parser or tokenizer class inside the jsBeautify() function, just a single loop shaped like for(let i=0;i<code.length;i++). It's fast and the code is short, but in exchange there's no guarantee whatsoever that it "understands the grammar."
2. What exactly determines indentation depth
Looking directly at the code, the depth variable is incremented by 1 only when it hits an opening brace {, and decremented by 1 on a closing brace } — that's the entire rule. Parentheses ( ) and square brackets [ ] play no part in this counter at all. So even deeply nested parentheses like if(a&&(b||c)) leave the indentation depth unchanged. Strings (quoted segments) are the one thing protected by a separate check called inStr(), so a { or ; inside a string passes through safely.
3. The exact mechanism that breaks for loops
The problem is the semicolon rule. The code has no state tracking "am I inside parentheses or not" — whenever it hits a ; character, it unconditionally inserts a line break. But a for(let i=0;i<n;i++){...} header contains exactly 2 semicolons (3 if you count the statement-terminating one that follows). The scanner has no way to tell whether these semicolons are "separators inside a for-loop header" or "the end of a statement," so it inserts a line break every single time. The result: a for-loop header that should be one line gets split into three.
| Stage | Content |
|---|---|
| Input | for(let i=0;i<10;i++){sum+=i;} |
| Expected output (manually cleaned up by a human) | for(let i=0;i<10;i++){
sum+=i;
} |
| Actual output (brace-counting approach) | for(let i=0;
i<10;
i++){
sum+=i;
} |
This isn't syntactically wrong (JS allows a line break after a semicolon), but it breaks the readability convention that a for-loop header should stay on one line. The more complex the header — say, a function call mixed into the condition — the worse the readability cost gets.
4. Another trap: regex literals
An even more dangerous case is regular expressions. inStr() is only designed to recognize quotes (', ", backtick), so a slash-delimited regex literal like /\{[0-9]+\}/ gets no protection at all. In that case, the { and } inside the regex are mistaken for a code block's opening/closing braces, and the depth counter increments or decrements incorrectly — which can cascade into misindented output for all the real code that follows. This asymmetry — strings are safe, regexes are not — is the tool's biggest limitation.
5. So when is it safe to use, and when is it risky?
For quickly skimming short, simple function- or object-heavy code, this approach is plenty practical. But when cleaning up algorithm-heavy code full of for loops, or validation logic mixed with regular expressions, don't trust the result blindly — check it by eye. If you're reversing minification, the JSON Minifier and CSS Beautifier use a similar character-counting structure, so the same caution applies there. If you want to validate a regex pattern itself ahead of time, we'd recommend testing it separately with the Regex Generator.
FAQ
Q. Does this formatter actually execute the code or check for syntax errors?
No. It only scans characters — it never runs the code through a JS engine or parses it — so if you feed it code that already had a syntax error, it won't tell you; it just tries to insert line breaks anyway.
Q. Does an array literal like [1,2,3] affect indentation the same way braces do?
No. The depth counter only tracks how many times curly braces { } appear — square brackets aren't counted separately. So commas inside an array often just get a space added with no line break, staying on one line, as long as the current brace depth is 0.
Q. Does a tool like Prettier avoid this problem entirely?
Prettier actually parses the code into a real syntax tree and re-emits it from that tree, so it understands context all the way down to for loops, regexes, and template literals. The trade-off is that a lightweight tool meant to run instantly in a browser with no install doesn't carry a full parsing engine like that.
Q. Are ${} expressions inside a template literal affected too?
A string that starts with a backtick is copied through as one whole string until the closing backtick appears, so any braces or semicolons inside its interpolated expressions are left untouched and never get reformatted.