Compatibility

Does Obfuscation Break try, catch and finally?

Error handling is where a compatibility defect does the most damage, because the code that runs when something has already gone wrong is the code with the least test coverage. JavaScript's try statement also has more sharp edges than people expect: finally can override a return, discard an exception, and run once per loop iteration. We measured all of it against five protection configurations, and every line matched.

The rules being tested

The sample is built around the parts of the try statement that people get wrong, because those are the parts an engine is most likely to get wrong too. If protection rewrote control flow even slightly, these are the cases that would show it.

It covers a finally that overrides a return, a finally that does not, a finally that swallows a pending throw, the full ordering of a nested try with a rethrow, a finally running once per loop iteration alongside continue and break, a break inside finally discarding an in-flight exception, optional catch binding, the scoping of the catch parameter, rethrow identity, a throw from inside finally replacing the pending error, non-Error throws of five different types, an accumulator that counts failures, a throw crossing three call frames, and the rule that a return value is captured before finally runs.

All five configurations produced identical output: ES5, modern, both identifier-renaming presets, and the string table. Member renaming pointed at the sample's own carrier fields was also identical on both targets, which is the clean result, and a slightly unusual one for this site.

The results that would catch a rewrite

The return-override rule holds. A function returning "from-try" with a finally that returns "from-finally" produced from-finally in every build, and a finally that only logs left the original return value alone. A finally that returns while an exception is pending still swallows that exception and returns normally.

Nested ordering is exact. The sample's nested try with a rethrow produced t1>t2>c2:inner>f2>c1:rethrown>f1 identically before and after: the inner catch runs, the inner finally runs before the outer catch sees the rethrown error, and the outer finally runs last. That single string would break under almost any restructuring of the statement.

Loop interaction is unchanged. A finally inside a for loop ran once per iteration, including the iteration cut short by continue and the one ended by break. A break placed inside finally still discards the exception that was propagating, which is one of the strangest rules in the language and one an engine could easily lose.

Identity survives. A rethrown error is the same object: err === original was true, and instanceof TypeError and instanceof Error both still narrowed correctly. A throw from inside finally still replaces the pending error rather than merging with it. Throwing a string, a number, an object, null and undefined all produced the same types and stringifications as before.

Catch binding and the value-capture rule

Optional catch binding, written as catch { } with no parameter, is preserved on both targets. It is modern syntax that has to be downlevelled for the ES5 target, and the downlevel produced identical behaviour.

The catch parameter is still scoped to its own block. A catch (e) that shadows an outer e leaves the outer binding untouched after the block ends, and our sample confirms the outer value is unchanged. Renaming operates within scopes, so this is exactly the property that a scope-aware renamer must not disturb, and it did not.

The value-capture rule holds as well: a function that returns box.n and then mutates box.n in finally still returns the original value, because the return value is captured before finally runs. Protected builds returned 1, not 99.

Errors still cross call frames intact. A RangeError thrown three functions deep arrived at the top-level catch with its name and message unchanged in every configuration.

What this does not tell you

Two boundaries are worth stating plainly, because a page that only reports good news is not much use.

First, this measures the try statement, not error objects. Custom error classes that extend Error are a separate question with a separate history on this site, and if you subclass Error and also use member renaming you should read what JavaScript obfuscation does not rename before shipping. The try statement itself is clean; the objects flowing through it have their own considerations.

Second, this measures behaviour, not readability. Stack traces from a protected bundle name the renamed functions, not your originals, so a caught error that you log or report will point at _0x4f rather than processPayment. That is the intended effect of the tool rather than a defect, and it is why symbol maps exist. Your catch blocks still run; what they print is less useful to a human reading a log.

If you rely on parsing stack strings to make decisions at runtime, that is string-based reflection over names the build step deliberately changes, and it will not survive. Decide on error name or an explicit code field instead.

What to do with this

Run your existing tests against the protected bundle. Error paths deserve the same treatment as happy paths, which means deliberately triggering the failures your catch blocks exist for rather than assuming they compile the same.

The specific thing worth asserting once, if your code depends on it, is the finally ordering in whatever your most complicated handler is. Not because we found a problem, but because that ordering is the property that would reveal a restructuring defect earliest, and it costs one test to pin.

Keep MemberRegexp away from names the language owns. In this area the ones to avoid are message, name, stack and cause: they are properties of every Error, and renaming them detaches your handling code from the objects the runtime creates.

If you need readable stack traces from protected production code, keep the symbol map from the build that produced the bundle. Without it the traces are not recoverable, and that is by design.

Frequently asked questions

Does obfuscation change try, catch and finally behaviour?

No. We measured the return-override rule, nested handler ordering with a rethrow, per-iteration finally with break and continue, optional catch binding, catch parameter scoping, rethrow identity and non-Error throws against protected copies on five configurations covering both targets, two identifier-renaming presets and the string table. Every line was identical.

Does a finally block still override a return?

Yes, in both directions. A finally that returns replaces the value the try block was returning and also swallows a pending exception; a finally that does not return leaves the original value alone. Both produced the same results after protection, including the rule that the return value is captured before finally runs.

Is optional catch binding supported?

Yes. catch with no parameter is preserved on the modern target and downlevelled correctly for the ES5 target, and both produced identical behaviour in our sample. The catch parameter also stays scoped to its own block, leaving any outer binding of the same name untouched.

Do stack traces still work in obfuscated code?

The mechanism works and the trace is produced, but the function names in it are the renamed ones. That is the point of the tool rather than a defect. Keep the symbol map from the build that produced the bundle if you need to read production traces, and avoid making runtime decisions by parsing stack strings.

Does rethrowing an error preserve its identity?

Yes. A rethrown error is the same object, so an identity comparison against the original still holds and instanceof still narrows to the right error type. A throw from inside finally still replaces the pending error rather than combining with it, exactly as in unprotected code.

Can member renaming break my error handling?

Yes, if the pattern reaches names the language owns. message, name, stack and cause are properties of every Error object, so renaming them disconnects your handlers from the objects the runtime creates. Anchor MemberRegexp to your own field names.

Related reading