Correctness

Does obfuscation preserve “use strict”?

It should, and it is easy for it not to. "use strict" is not a statement that does something — it is a directive prologue, and it only counts if it is literally the first thing in the file or function body. Almost everything an obfuscator does to the top of a file destroys that property, and when it happens nothing errors. Your whole bundle just quietly switches to sloppy mode.

The rule, precisely

A directive prologue is a run of expression statements that are string literals, at the very start of a program or a function body, before any other statement. Three consequences follow, and each is a way to lose it:

  • Anything before it demotes it. One var declaration ahead of the directive and it becomes a stray string expression that evaluates and is discarded.
  • It has to be a literal. "use strict" works. _0x4A1[0] does not, even if that array element contains exactly the text use strict. The check is syntactic, at parse time.
  • Escapes count against you. "use strict" has the right value but is not a directive, because the rule is defined on the literal's raw source text. A string-escaping transform applied to the prologue therefore disables it while leaving the output looking correct.

Why obfuscators are unusually good at breaking it

Look at the three most common transforms and what each does to the top of a file.

  • String extraction. Every literal moves into a lookup array declared at the top of the file. That declaration lands above the directive — and the directive itself is a string literal, so a naive implementation also moves it into the table and replaces it with an index expression. Two independent kills at once.
  • Whole-program wrapping. Self-defending code, self-compression and eval-based packers wrap the program in a function. The directive is now inside a function body, and whether it survives depends entirely on whether it stayed at position zero of that body.
  • Statement reordering and hoisting. Any pass that moves declarations to the top of a scope will happily move one above the directive.

These are all reasonable things to do. Each of them just needs to know about one special case, and that case is invisible unless someone thought to test for it. We have found and fixed several of them in our own engine, which is the reason this article exists rather than a hypothetical one.

What actually changes if you lose it

This is the part that decides whether you should care. Sloppy mode is not “strict mode with the warnings off” — it is different semantics:

  • this in a plain function call. Strict: undefined. Sloppy: the global object. Any code doing this.x in a detached method now reads and writes globals instead of throwing.
  • Assignment to an undeclared name. Strict: ReferenceError. Sloppy: creates a global. A typo that used to be a loud crash becomes a leaked global and an unrelated bug elsewhere.
  • Writing to a frozen or read-only property. Strict: TypeError. Sloppy: silently does nothing. Code that validated its own invariants by expecting a throw stops validating.
  • Deleting an unqualified name, duplicate parameter names, octal literals. SyntaxErrors in strict, accepted in sloppy.
  • arguments aliasing. In sloppy mode arguments[0] and the first parameter are linked; assigning to one changes the other. In strict they are independent.

Notice the direction of every one of these: sloppy mode is more permissive. Nothing crashes when you lose strict mode. That is exactly what makes it dangerous — the failure surfaces later, somewhere else, as a mysterious global or an invariant that stopped being enforced.

A one-minute probe

Append this to any file that starts with "use strict", run it before and after your build, and compare:

function probeThis()   { return this === undefined ? "strict" : "sloppy"; }
function probeGlobal() { try { undeclaredThing = 1; return "sloppy"; }
                         catch (e) { return "strict"; } }
function probeFrozen() { var o = Object.freeze({ a: 1 });
                         try { o.a = 2; return "sloppy"; }
                         catch (e) { return "strict"; } }

console.log(JSON.stringify([probeThis(), probeGlobal(), probeFrozen()]));

// With the directive:     ["strict","strict","strict"]
// Without the directive:  ["sloppy","sloppy","sloppy"]

Three probes rather than one on purpose. They test different mechanisms — receiver binding, scope resolution, and property-write semantics — and a partial failure tells you more than a single boolean would. All three must read strict.

If you would rather not modify the file, a source-level check gets most of the way: confirm the emitted output begins with the literal characters "use strict" or 'use strict', before any whitespace-insensitive parsing. It cannot catch a directive that survived at the top of the file but was lost inside a wrapper function, which is why the runtime probe is better.

Three cases where you do not need to worry

  • ES modules are always strict. If your output is a module — .mjs, or <script type="module"> — strict mode is on regardless, and the directive is redundant. A tool that drops it there has changed nothing.
  • Class bodies are always strict, including everything defined inside them.
  • Per-function directives are independent. function f(){ "use strict"; ... } is strict even if the file is not — but the same first-statement rule applies inside the body, so a pass that hoists a variable into that function will break it the same way.

What to ask a vendor

Not “do you preserve strict mode” — everyone says yes. Ask whether their test suite asserts it, and specifically whether it asserts it under the heavier modes: with string extraction on, with self-defending or compression wrapping enabled, and for a directive inside a function body rather than at the top of the file. Those are separate code paths, and in our experience a tool can get the simple case right and the wrapped case wrong, because the simple case is the one anybody thinks to check.

Ours now has gate coverage for each of those combinations, added after we discovered we did not. That is the honest version of the answer, and it is the version worth asking for.

Related reading