Compatibility

Does Obfuscation Change Evaluation Order?

This is the compatibility question that should worry you most, because it is the one whose failures would be silent. If a tool rewrites your code and quietly changes when a side effect fires, no error is thrown and no test necessarily catches it; you just get a different number somewhere downstream. We built a sample whose entire purpose is to expose reordering, ran it through five protection configurations, and diffed every line.

Why this sample is designed to fail

Most compatibility checks confirm that a feature still works. This one is different: it is built so that a reordering engine would produce visibly wrong output. Every case records the order in which its subexpressions ran, so a rearrangement shows up as a changed string rather than as a subtle numeric drift.

The sample logs the order of argument evaluation, compares postfix against prefix increment, exercises compound assignment against a side-effecting target expression, uses the comma operator, checks that short-circuit operators skip their right-hand side entirely, checks that a ternary evaluates exactly one branch, counts how many times a getter fires, and records what has already happened when an exception interrupts an expression halfway through.

All five configurations produced output identical to the original, line for line: the ES5 target, the modern target, the two identifier-renaming presets our emit-validation gate uses, and the string-table preset that moves and encodes every literal.

The results, case by case

Arguments evaluate strictly left to right, and still do. Calling a three-parameter function with three side-effecting expressions logged a,b,c before and after protection. The same holds inside array literals and object literals: the elements and the property values run in source order.

The increment operators keep their distinction. Postfix returned 5 and left the variable at 6; prefix returned 6 and left it at 6. The trap that depends on this, k = k++, still leaves k at 0 rather than 1, which is the correct and widely misunderstood behaviour.

Compound assignment still evaluates its target expression exactly once. We wrote target().n += 5 where target() increments a counter, and the counter read 1 both before and after protection. An engine that naively expanded that into target().n = target().n + 5 would have shown 2, and none of the five configurations did.

Short-circuiting still short-circuits, which is the case with the most real-world consequence. false && sideEffect() and true || sideEffect() both recorded zero side effects after protection. Guards written as user && user.save() therefore keep their meaning. The ternary likewise evaluated exactly one branch and logged only taken.

Getters fire on read, once per read. Reading the same accessor property twice in one expression produced two hits and the sum 14, unchanged. Chained assignment still binds right to left. The delete operator still returns true and still removes the property.

What an interrupted expression leaves behind

The most interesting case is the one people never test. When an expression throws partway through, the side effects that already ran are not rolled back, because JavaScript has no such mechanism.

Our sample evaluates a side-effecting call, then a function that throws, and catches the result. Before protection the log holds before; after protection, on every configuration, the log still holds before. The earlier effect happened and stayed happened.

This matters if you have written recovery code around a throwing expression. Whatever assumptions it makes about partial completion remain exactly as valid, or as invalid, as they were in the original source. Protection neither improves nor worsens that situation, which is the answer you want.

Why the transforms cannot reach this

The result is not luck, and the reason is worth stating because it generalises to questions we have not measured yet.

Identifier renaming substitutes one name for another within a scope. It changes what things are called, not the order in which the expressions containing them are evaluated. The string transforms replace a literal with a lookup that returns the same value at the point the literal used to sit. Neither operation has any reason to move a subexpression across a sequence point, and neither does.

The related evidence is in math and floating point, where the non-associativity of addition survives protection intact. If the engine were re-grouping or re-parenthesising arithmetic, (0.1 + 0.2) + 0.3 and 0.1 + (0.2 + 0.3) would have converged on the same value. They did not, which is direct evidence that expression structure is preserved rather than merely appearing to be.

What to do with this

For code whose correctness depends on ordering — transaction steps, audit counters, anything where a getter or a setter does real work — you do not need an ordering-specific test plan for the protection step. Your existing tests, run against the protected bundle, are sufficient evidence.

The habit worth keeping is unrelated to obfuscation: expressions that hide side effects inside operands are hard to read and hard to review, and they stay hard after protection. If a line of yours only works because of the order its subexpressions run in, that line is a maintenance problem independent of any tool.

If you want to confirm this against your own code rather than accept our measurement, the general recipe is in verifying the obfuscator did not break your code. Ordering is one of the easier properties to assert on, because a counter or an append-only log makes it visible.

Frequently asked questions

Can obfuscation reorder my code?

Not in any way we could measure. We built a sample specifically to expose reordering, logging the sequence in which every subexpression ran, and diffed it against protected copies on five configurations covering both the ES5 and modern targets, two identifier-renaming presets and the string table. Argument order, array and object literal order, and binary operand order were identical in all of them.

Does protection change when side effects fire?

No. Getters fired the same number of times, short-circuit operators still skipped their right-hand side entirely, and a ternary still evaluated exactly one branch. Zero side effects were recorded on the skipped paths after protection, exactly as before.

Is compound assignment expanded into something that evaluates twice?

No. We tested this directly because it is the plausible way an engine could get it wrong. Using a side-effecting target expression, the target was evaluated once before protection and once after, on every configuration. A naive expansion would have evaluated it twice and the counter would have shown it.

Does the postfix increment still behave the same way?

Yes, including the case people find counter-intuitive. Postfix returns the old value and prefix returns the new one, and the classic k = k++ still leaves k at 0 rather than 1. That behaviour is a property of the language and is preserved through protection.

If an expression throws halfway through, do earlier side effects survive?

Yes, exactly as they do in unprotected JavaScript, which has no mechanism to roll them back. Our sample confirmed that the effect which ran before the throw was still recorded afterwards on every configuration. Protection does not change the partial-completion behaviour your recovery code sees.

Why does operator precedence matter for testing an obfuscator?

Because it is a structural property rather than a value, so it detects a whole class of defect that value-based tests miss. If protection re-grouped expressions, non-associative arithmetic would converge and unparenthesised mixed-precedence expressions would change value. Both were checked and neither moved, which is stronger evidence than any single feature test.

Related reading