skip to content

You add "use strict" to the top of a large legacy browser script and parts of the page stop working. What classes of failure should you expect, and how would you roll the change out safely?

level: seniorimportance: should knowfreq 35%

answer

  1. split by parse time versus runtime
  2. a syntax error kills the whole file
  3. runtime breakage is path-dependent
  4. per-function directive is the migration unit
  5. check how files are concatenated

basics

~20 s

Expect two classes: parse-time syntax errors that kill the entire script before a line runs, and runtime errors on code paths that previously failed quietly. Roll out per function rather than per file, and lint for undeclared globals first.

solid answer

~60 s

Separate the failures by *when* they happen. Some strict-mode violations are early errors: legacy octal literals, `with`, duplicate parameter names, `delete someVariable`, and using `eval` or `arguments` as a binding name. Those make the whole script fail to parse, so nothing in the file runs — the symptom is "the page is completely dead", not "one feature broke". Everything else surfaces at runtime, only on the paths that execute: `ReferenceError` on an undeclared assignment, `TypeError` on a write that used to fail silently or on `this` being `undefined` in a plain call, and `arguments.callee` or `fn.caller` throwing. Two subtler behaviour changes bite without any error at all — `arguments` stops aliasing named parameters, and a direct `eval` gets its own scope so its `var` declarations no longer leak. My rollout is incremental: lint first with `no-undef` and friends to find implicit globals statically, then apply the directive one function at a time rather than to the file, and move files to modules where I can, since module code is strict by construction.

code

javascript · 10 lines
javascript
"use strict";

if (true) {
  function helper() {
    return "block scoped under strict mode";
  }
  console.log(typeof helper);
}

console.log(typeof helper);

go deeper

for a junior

Recall the headline breakages: undeclared assignments now throw ReferenceError, and constructs like with and octal literals are outright syntax errors that stop the file from loading.

for a middle

Explain which violations are early errors versus runtime errors, and why that distinction changes the symptom — a dead page versus one broken feature — and therefore where you look first.

for a senior

Lay out an actual rollout: lint for implicit globals first, grep for the early-error constructs, convert one function at a time, verify with production error reporting, and account for how the files are bundled.

for a principal

Frame the end state rather than the patch — moving the codebase to modules so strictness stops being a per-file decision, and deciding how much legacy risk is worth carrying versus rewriting the offending files outright.

## Sort the failures by when they fire The single most useful move in this scenario is to split strict-mode breakage into *early errors* and *runtime errors*, because they have completely different symptoms and completely different debugging strategies. ### Early errors — the whole script dies A strict-mode early error is raised while parsing, before any statement executes. In a browser that means the entire script element fails and nothing it defined exists. If your symptom is "every feature on the page is gone", look here first; the console will show a single `SyntaxError` with a line number. What becomes a syntax error under strict mode: - **Legacy octal literals and octal string escapes** — `010`, `"\251"`. Common in old permission masks and copyright strings. - **The `with` statement** — banned outright. - **Duplicate parameter names** — `function f(a, a) {}`. - **`delete` on a plain identifier** — `delete someVar`. - **`eval` or `arguments` used as a binding name** — as a parameter, a `var`, a function name, or an assignment target. - **Future reserved words as identifiers** — `implements`, `interface`, `package`, `private`, `protected`, `public`, `static`, `let`, `yield`. ### Runtime errors — one code path at a time Everything else waits for execution, so the breakage is partial and only shows up when a user reaches that path. This is why "it worked in my smoke test" is not evidence. - **`ReferenceError` on undeclared assignment.** Any variable a file relied on creating implicitly now throws at the moment of the write. - **`TypeError` on a failed write.** Assigning to a frozen or non-writable property, to a getter-only accessor, or adding to a non-extensible object. - **`TypeError` from `this` being `undefined`.** A helper called plainly that assigns to `this` used to write to the global object and now throws. - **`TypeError` on legacy reflection.** `arguments.callee`, `fn.caller` and `fn.arguments` are poisoned accessors in strict code — old recursive anonymous functions and stack-walking utilities break here. ### Silent behaviour changes — no error at all The worst category, because tests pass and behaviour differs: - **`arguments` stops aliasing named parameters.** In a sloppy function, writing `arguments[0] = 5` also changes the parameter `a`. In strict mode the two are independent snapshots. - **Direct `eval` gets its own variable scope.** `eval("var x = 1")` no longer creates `x` in the surrounding function, so any code that relied on eval to inject variables quietly stops working. - **Block-level function declarations become block-scoped.** In sloppy browser code the Annex B web-compatibility rules hoist a function declared inside an `if` out to the enclosing function scope. In strict mode it is scoped to the block and is gone afterwards: ```javascript "use strict"; if (true) { function helper() { return 1; } console.log(typeof helper); // "function" } console.log(typeof helper); // "undefined" ``` ## The concatenation hazard Before blaming your own file, check how it is delivered. The script-level directive covers the whole script, so if legacy files are glued together, a directive in the first file silently makes all of them strict — including third-party code you never intended to convert. The mirror image also happens: a file that was written strict gets pasted into the middle of a bundle, loses its prologue position, and quietly reverts to sloppy semantics with no diagnostic at all. Any combining step must wrap each file in its own function or emit real modules. ## A safe rollout 1. **Lint statically first.** Undeclared assignments are the most common breakage and are findable without running anything — a `no-undef`-style rule plus an explicit list of the globals the page really provides. Fix those *before* touching the dialect; the fixes are valid in sloppy mode too, so they ship independently. 2. **Grep for the early-error constructs.** `with`, octal literals, `arguments.callee`, `.caller`, duplicate parameters. There are few of them and they are the ones that take the whole file down. 3. **Convert per function, not per file.** Put the directive at the top of one function body, ship, watch. Strictness is lexical, so the blast radius is that function and its nested functions, and the change survives concatenation. 4. **Make globals explicit.** Where a file genuinely needs to publish something, write `globalThis.name = value` instead of relying on an implicit assignment. That is clearer and works in both dialects. 5. **Prefer moving the file to a module.** Module code is strict with no directive, and it removes the concatenation hazard at the same time — usually the better end state than sprinkling directives. 6. **Verify in production, not just in tests.** Runtime failures are path-dependent, so ship behind whatever error reporting you have and watch for a spike in `ReferenceError` and `TypeError` before converting the next file. ## What the interviewer is listening for The parse-time versus runtime split, the fact that undeclared globals are findable statically, per-function scoping as the incremental unit, and awareness that concatenation moves the strictness boundary. A candidate who only says "you get errors for sloppy code" has not operated this migration.

  • Why does an early error make debugging a strict-mode migration easier than the runtime failures do?
    Because it is deterministic and total. A parse-time `SyntaxError` fires before anything executes, on every load, with one line number — so you find and fix it in the first page view. Runtime failures are path-dependent: they only appear when a user reaches the branch containing the undeclared assignment or the bad `this`, which is why static linting for those is worth doing before you flip the dialect.
  • A legacy utility relies on `arguments[0] = value` changing the named parameter. What happens after the directive is added?
    It stops working, silently. In sloppy mode `arguments` stays aliased to the named parameters, so writing through the array-like also updates the parameter binding. Strict mode breaks that link, leaving two independent values. No error is thrown, which is what makes it dangerous — the function just computes with the original argument. The fix is to assign to the named parameter directly.
  • You inherit a build that concatenates twenty legacy files into one script. What do you check before adding a directive to any of them?
    Whether the combining step wraps each file. If it does not, a directive at the top of the first file heads the whole concatenated script and silently converts all twenty — including vendor code. And a file already written strict loses its effect once it is no longer first. Wrap each file in a function, or move to real modules, before touching dialects at all.

saying these in an interview costs you the question

  • Assumes all strict-mode failures appear at runtime
  • Adds the directive file-wide and ships it untested
  • Ignores that concatenation moves the strictness boundary
  • Thinks strict mode only affects undeclared variables
  • Expects tests to catch path-dependent runtime breakage

context