Compatibility

Does obfuscation break labeled statements?

Labeled statements are the least fashionable part of JavaScript control flow and the part most likely to make someone nervous about a transform that renames things and rewrites loops. The concern is reasonable: a label is an identifier, and identifiers are what obfuscation renames. It turns out labels are handled as a separate namespace, and the measurements are clean.

What a label actually is

A label is not a variable. It does not live in a scope, cannot be read, cannot be assigned, and is only ever mentioned by a break or continue that names it. JavaScript keeps labels in their own namespace, which is why you can legally write x: for (const x of xs) and have the label and the binding not collide.

That separation is what makes them safe here. A rename pass works on bindings and on property names. A label is neither, so there is nothing for it to attach to, and a transform that renamed a label without renaming the matching break would produce code that fails to parse rather than code that silently misbehaves.

The measurement, including the interesting case

A nested loop labeled outer, containing both a continue outer and a break outer, was run through the protector in both output targets.

In the modern target the structure is preserved almost exactly: the loop bindings are renamed, the label stays outer, and both jump statements still name it. That much is unsurprising.

The case worth reporting is the default target, which rewrites a for...of loop into an index loop with different bindings and a different header. The label and its two targets moved with it correctly: the rewritten loop is still labeled outer, and the continue outer and break outer inside the inner loop still refer to it. A loop can change shape underneath a label without the label losing its meaning, which is the specific thing you would want to check.

The one case that does not survive the default target

There is a limit to the result above, and it is worth stating precisely because it is narrow and it is silent. Everything measured so far concerns control flow: which loop a jump targets, and which iterations run. That is correct on both targets. Closure capture inside a labelled nested loop is not.

The measurement: a for...of loop labelled outer, containing an inner loop that runs continue outer, with a function pushed onto an array on each surviving iteration. Unprotected, calling those functions afterwards yields 1:1,2:1. On the modern target the protected output matches exactly. On the default target it yields undefined:2,undefined:2 -- the outer binding is gone and the inner one has kept its post-loop value.

The mechanism is the per-iteration copy that gives let its capture semantics when a loop is rewritten into ES5. A labelled jump out of a nested loop leaves the outer loop without that copy, so the functions all close over one shared binding. The list of values the loop pushed is still correct, and the iteration order is still correct; only the functions created inside the loop are wrong.

This is a tracked defect with a regression sample rather than a permanent property of the output, so a current build is the thing to measure against. In the meantime the shape to avoid is specific: creating closures over loop bindings, inside a nested loop, under a label, while building for the default target. Building for the modern target avoids it, and so does lifting the function out of the loop into a factory called with the values it needs.

Where labels are worth keeping

Since they survive cleanly, the question becomes whether to keep using them, and the honest answer is that the alternatives are often worse for a protected build.

The usual way to avoid a label is a flag variable checked after the inner loop, or extracting the nested loop into a function and returning early. The flag adds a binding and a branch, which is more surface for a reader to follow but also more for you to get wrong. The extracted function is usually the cleanest refactor and has a real advantage under protection: a function boundary is a natural unit for the transforms to work with.

What you should not do is reach for a label as a way to make control flow harder to read. That is what the control-flow transforms are for, and they do it systematically rather than by hand. Hand-obfuscated source is source you still have to maintain.

The related things people ask about in the same breath

Two neighbours of this topic come up together often enough to state briefly. The with statement is not something to rely on: it is forbidden in strict mode and therefore in every ES module, and any code that depends on dynamic scope through it is hostile to every static analysis, including the ones a protector performs. If you still have a with block, removing it is worth doing for its own sake.

The debugger statement is a different matter and worth checking before a release. It survives the transform, so a stray one ships. That is separate from the runtime debug-protection option, which inserts its own checks deliberately; a leftover debugger from a debugging session is just a bug, and a build step that rejects them is a cheap thing to add.

Frequently asked questions

Are label names renamed by obfuscation?

No. Labels live in their own namespace, separate from variable bindings and property names, so the rename passes have nothing to attach to. Measured in both output targets, a label named outer stayed outer and both the continue and the break that named it still referred to it.

Do labels survive when the loop they label is rewritten?

Yes, and this is the case worth checking. The default output target rewrites a for-of loop into an index loop with different bindings and a different header. The rewritten loop is still labeled and the break and continue inside the nested loop still target it correctly.

Should I replace labels with flag variables before protecting?

There is no compatibility reason to. If you want to remove a label for readability, extracting the nested loop into a function and returning early is usually the better refactor, because a function boundary is a natural unit for the transforms. A flag variable simply adds a binding and a branch.

Can I use a label as a cheap form of obfuscation?

It is not worth it. The control-flow options do that systematically, while hand-tangled source is source you still have to read and maintain. Write control flow for the person who has to change it, and let the transforms handle making the shipped form harder to follow.

Does the with statement work in protected code?

It should not be in your code at all. with is forbidden in strict mode and therefore in every ES module, and code that relies on dynamic scope through it resists every kind of static analysis. Removing it is worthwhile independently of protection.

Will a stray debugger statement survive protection?

Yes. A debugger statement in your source is carried through, so one left from a debugging session will ship. That is unrelated to the runtime debug-protection option, which inserts its own checks on purpose. A build step that fails on a leftover debugger statement is a cheap safeguard.

Do closures inside a labelled loop capture the right values?

On the modern target, yes. On the default target this is the one measured exception to everything else on this page: a labelled continue or break that jumps out of a nested loop leaves the outer loop's per-iteration bindings uncopied, so functions created inside it read the values left behind after the loop rather than the values from their own iteration. Control flow is unaffected. If you close over loop variables inside a labelled nested loop, either build for the modern target or lift the closure out of the loop.

Related reading