Strict Mode & Common Pitfalls

JavaScript was designed in a hurry in 1995, and some of its early decisions turned silent mistakes into "features". Strict mode removes many of them. This lesson covers how strict mode works and the classic pitfalls — with and without it — that catch developers at every level.

Enabling strict mode

Put "use strict"; at the very top of a script or function. You often don't need to: ES modules and classes are always strict, so most modern code already is.

JS strict.js
"use strict";

let userName = "Ana";
usrName = "Ben";
Output
Uncaught ReferenceError: usrName is not defined

Without strict mode, that typo would silently create a new global variable called usrName, and userName would stay unchanged — a bug that can take hours to find.

What strict mode changes

SituationSloppy modeStrict mode
Assigning to an undeclared variableCreates a globalReferenceError
Writing to a read-only or frozen propertySilently ignoredTypeError
this in a plain function callThe global objectundefined
Duplicate parameter namesAllowedSyntaxError
with statements, octal literals like 010AllowedSyntaxError

Every change turns a silent failure into a visible error, which is exactly what you want.

Pitfall: == instead of ===

JS equality.js
console.log("" == 0, "0" == false, null == 0, [] == false);
console.log("" === 0, "0" === false, null === 0, [] === false);
Output
true true false true
false false false false

Loose equality's conversion rules are unpredictable. Use === everywhere.

Pitfall: floating-point maths

JS float.js
console.log(0.1 + 0.2 === 0.3);
console.log(1.005 * 100);
Output
false
100.49999999999999

Round for display and store money as integer cents (lesson 16).

Pitfall: string concatenation with numbers

JS concat.js
const fromInput = "5";
console.log(fromInput + 1);
console.log(Number(fromInput) + 1);
Output
51
6

Pitfall: mutating shared objects

JS mutation.js
const defaults = { tags: [] };
const a = { ...defaults };
const b = { ...defaults };

a.tags.push("urgent");
console.log(b.tags);
Output
[ 'urgent' ]

Spread copies are shallow: a.tags and b.tags are the same array. Use structuredClone, or create fresh nested values each time.

Pitfall: sorting numbers

JS sort.js
console.log([10, 9, 1, 100].sort());
console.log([10, 9, 1, 100].sort((a, b) => a - b));
Output
[ 1, 10, 100, 9 ]
[ 1, 9, 10, 100 ]

Pitfall: automatic semicolon insertion and return

JS asi.js
function getConfig() {
  return
  {
    debug: true;
  }
}
console.log(getConfig());
Output
undefined

A line break right after return ends the statement, so the function returns undefined and the object below is never reached. Always start the returned value on the same line as return.

Pitfall: losing this

JS this.js
"use strict";
const counter = {
  count: 0,
  increment() {
    this.count++;
  },
};

const fn = counter.increment;
try {
  fn();
} catch (err) {
  console.log(err.constructor.name, "— this was undefined");
}
setTimeout(() => counter.increment(), 0);
setTimeout(() => console.log("count:", counter.count), 5);
Output
TypeError — this was undefined
count: 1

Passing a method as a callback detaches it from its object. Wrap it in an arrow function or use bind (lesson 27).

Pitfall: forgetting await

JS await.js
async function getTotal() {
  return 42;
}

const total = getTotal();
console.log(total + 1);
Output
[object Promise]1

Without await, you get the promise, not its value. Linters can catch this ("no-floating-promises"), and TypeScript catches it automatically.

Pitfall: typeof null and arrays

JS typeof.js
console.log(typeof null, typeof [], Array.isArray([]));
Output
object object true

Tools that catch these for you

  • ESLint — flags ==, unused variables, unreachable code, missing awaits and hundreds of other problems as you type.
  • Prettier — formats code consistently, which makes ASI surprises and mismatched braces obvious.
  • TypeScript — adds types, catching wrong-type bugs before the code runs (see the TypeScript course).