Left-Pad Refactoring Lecture
A live walkthrough of the famous npm left-pad module — starting from the original 2016 source and a suite of contract tests, then replacing the whole module with one ES2017 built-in (`String.prototype.padStart`) and watching exactly one test break.
The Module That Broke the Internet
Why this matters
In March 2016, a developer un-published an eleven-line npm package called left-pad. Within hours, Babel, React, and thousands of other projects stopped building — npm install couldn’t resolve the transitive dependency. The incident kicked off years of debate about supply-chain risk, micro-dependencies, and what belongs in a standard library. This lecture replays the story by reading the actual code, running its tests, then replacing the whole module with one line of modern JavaScript.
🎯 You will learn to
- Read a tiny but production-shipped JavaScript module and identify its contract
- Apply Node’s built-in
assertto encode a contract as runnable tests - Evaluate what behaviors a test suite actually protects vs. assumes
The original leftPad
The pre-incident left-pad package was famously short — variations weighed in between 8 and 17 lines. The version below is the canonical one from before the optimisation patches. leftPad(str, len, ch) left-pads str with ch until it reaches length len:
function leftPad (str, len, ch) {
str = String(str);
var i = -1;
if (!ch && ch !== 0) ch = ' ';
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}
| Line | What it does |
|---|---|
str = String(str) |
Coerce non-string input (numbers, booleans) into a string — this matters in step 2 |
if (!ch && ch !== 0) ch = ' ' |
Default pad character is a space; but 0 is allowed even though it’s falsy |
len = len - str.length |
Reuse len as the number of pad characters still to prepend |
while (++i < len) |
Prepend ch once per missing character |
The contract — as tests
The test file on the right pane encodes the main contract using Node’s built-in node:assert. Each test(...) block calls leftPad and asserts on the result. The runner prints ✓ or ✗ per test plus a N passed, M failed line at the end.
✏️ Predict before you run
Look at the six tests in leftpad.test.js. Which of these is most likely to behave differently if we later replace leftPad with a built-in language feature?
- (a)
pads with spaces by default— the default-' 'case - (b)
pads with a custom character— the canonical happy path - (c)
returns the string unchanged when length is already met— the no-op case - (d)
accepts a number as input (coerces to string)— relies onString(str)
Commit to a letter, then click ▶ Run.
Reveal (after committing)
**(d)** is the trap. The other five tests describe behavior any string-padding function would have — including a built-in. (d) leans on a JavaScript-specific coercion the original module performs explicitly (`str = String(str)`). A naive built-in replacement won't do this. We'll watch it break in step 2.Task
- Read
leftpad.js(left pane) andleftpad.test.js(right pane). - Click ▶ Run to execute
node leftpad.test.js. - Confirm all six tests pass.
// The original left-pad — the eleven-line module that broke the
// internet in March 2016. Reproduced here verbatim from the
// pre-incident package.
//
// Pads `str` on the LEFT with character `ch` until its total
// length is `len`. If `str` is already that long (or longer),
// it is returned unchanged.
function leftPad (str, len, ch) {
str = String(str);
var i = -1;
if (!ch && ch !== 0) ch = ' ';
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}
module.exports = leftPad;
// Contract tests for the leftPad module.
//
// Each `test(name, fn)` call runs `fn()` and reports ✓ if it
// completes without throwing, ✗ otherwise. The body uses Node's
// built-in `node:assert` module (no third-party test framework
// required).
//
// Run with: node leftpad.test.js
const assert = require('node:assert');
const leftPad = require('./leftpad');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(' ✓ ' + name);
passed++;
} catch (e) {
console.log(' ✗ ' + name);
console.log(' ' + e.message);
failed++;
}
}
console.log('Running leftPad contract tests:\n');
// --- The main contract -------------------------------------------
test('pads with spaces by default', () => {
assert.strictEqual(leftPad('abc', 5), ' abc');
});
test('pads with a custom character', () => {
assert.strictEqual(leftPad('abc', 5, '0'), '00abc');
});
test('returns the string unchanged when length is already met', () => {
assert.strictEqual(leftPad('hello', 5, '0'), 'hello');
});
test('returns the string unchanged when target length is shorter', () => {
assert.strictEqual(leftPad('hello', 3, '0'), 'hello');
});
test('handles single-character padding to a wider target', () => {
assert.strictEqual(leftPad('x', 4, '*'), '***x');
});
// --- The JS-specific coercion the original module performs -------
//
// The original leftPad starts with `str = String(str)`, so it
// accepts numbers and coerces them to a string. A naive
// replacement that calls `.padStart()` directly on `str` will
// break this test, because numbers do not have a .padStart()
// method. See step 2.
test('accepts a number as input (coerces to string)', () => {
assert.strictEqual(leftPad(7, 4, '0'), '0007');
});
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);
Step 1 — Knowledge Check
Min. score: 80%
1. The original leftPad starts with str = String(str). If you removed that line and called leftPad(7, 4, '0'), what would happen?
(7).length is undefined, so len - str.length is NaN, and ++i < NaN is always false. The function returns the original number unchanged — definitely not a padded string. The explicit str = String(str) line is what makes the “accepts a number” test pass.
2. The test suite calls assert.strictEqual(leftPad('hello', 3, '0'), 'hello'). What contract does this test encode?
The contract is one-sided: pad only when the input is shorter than the target length; otherwise return the input as-is. That’s why 'hello'.padStart(3) behaves identically — padStart follows the same “no-truncation” rule, which is part of why it’s a valid replacement candidate.
One Built-In Replaces Eleven Lines: String.prototype.padStart
Why this matters
The reason left-pad no longer needs to exist is ES2017 (a.k.a. ES8). That language revision added String.prototype.padStart(targetLength, padString) and its sibling padEnd to the standard library. Node.js 8+ ships them. Two million npm dependents could replace the whole module with one method call — if they’re willing to accept the spec-correct semantics that come with it.
🎯 You will learn to
- Apply
String.prototype.padStartto replace the body ofleftPad - Analyze which behaviors are preserved and which break under the swap
- Evaluate when “spec-correct” differs from “what the previous implementation actually did”
String.prototype.padStart
'abc'.padStart(5) // ' abc'
'abc'.padStart(5, '0') // '00abc'
'hello'.padStart(3) // 'hello' ← no truncation, matches leftPad
Three reasons this is “the most efficient Node.js call that replaces left-pad”:
- Zero dependencies — it’s part of the language runtime.
- Zero bytes shipped — no
npm install, norequire. - Implemented in V8 C++ — orders of magnitude faster than a
whileloop allocating new strings each iteration.
The naive swap
The temptation is to drop the body of leftPad and forward directly to the built-in:
function leftPad (str, len, ch) {
return str.padStart(len, ch);
}
Two lines. The whole module.
✏️ Predict before you run
How many of the six tests still pass?
- (a) All six —
padStartis the standard library version of exactly this function. - (b) Five — one test relies on a JS-specific behavior the built-in doesn’t replicate.
- (c) Four —
padStarthas different default behavior for the pad character. - (d) Zero —
padStarttruncates instead of preserving short strings.
Commit to a letter, then click ▶ Run and read the output carefully.
Reveal (after running)
**(b)**. Five of the original six pass. The one that breaks is `accepts a number as input` — `leftPad(7, 4, '0')` throws `TypeError: str.padStart is not a function`, because `(7).padStart` is `undefined`. Numbers don't have the method; only strings do. The original module masked this with its first line, `str = String(str)`. The naive swap dropped that line — so the contract for non-string input is gone.Task
- Replace the body of
leftPadinleftpad.jswithreturn str.padStart(len, ch); - Click ▶ Run. Five tests should pass, one should fail.
- Read the failing test’s name and error message. Confirm it’s the number-coercion case.
- Step 3 will fix it — but first, understand the break.
// The original left-pad — soon to be replaced with one line.
//
// 🛠️ TASK: Replace the body of leftPad with:
//
// return str.padStart(len, ch);
//
// Then click ▶ Run. Five tests pass, one fails.
function leftPad (str, len, ch) {
str = String(str);
var i = -1;
if (!ch && ch !== 0) ch = ' ';
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}
module.exports = leftPad;
// Same contract tests as step 1 — unchanged. The point is to
// watch the test suite respond to a change in `leftpad.js`.
const assert = require('node:assert');
const leftPad = require('./leftpad');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(' ✓ ' + name);
passed++;
} catch (e) {
console.log(' ✗ ' + name);
console.log(' ' + e.message);
failed++;
}
}
console.log('Running leftPad contract tests:\n');
test('pads with spaces by default', () => {
assert.strictEqual(leftPad('abc', 5), ' abc');
});
test('pads with a custom character', () => {
assert.strictEqual(leftPad('abc', 5, '0'), '00abc');
});
test('returns the string unchanged when length is already met', () => {
assert.strictEqual(leftPad('hello', 5, '0'), 'hello');
});
test('returns the string unchanged when target length is shorter', () => {
assert.strictEqual(leftPad('hello', 3, '0'), 'hello');
});
test('handles single-character padding to a wider target', () => {
assert.strictEqual(leftPad('x', 4, '*'), '***x');
});
test('accepts a number as input (coerces to string)', () => {
assert.strictEqual(leftPad(7, 4, '0'), '0007');
});
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);
Step 2 — Knowledge Check
Min. score: 80%
1. Why does the naive replacement return str.padStart(len, ch); cause leftPad(7, 4, '0') to throw rather than return '0007'?
JavaScript’s method dispatch follows the prototype chain. 7.__proto__ is Number.prototype, which has no padStart. The expression (7).padStart evaluates to undefined, and undefined('0') throws TypeError: str.padStart is not a function. The fix (step 3) is to coerce to string first — String(7) has padStart via String.prototype.
2. Five of the six original tests still pass after the naive swap. Which observation best explains why the standard-library replacement is a legitimate candidate even though one test breaks?
The strength of a refactoring candidate is judged by what it gets right by default. padStart matches the hard parts (default pad character, no-truncation, repeated padding to fill the gap) with zero glue code. Only one behavior — implicit input coercion — needs to be added back. That’s a one-line fix, which is exactly what step 3 demonstrates.
Restore the Contract — String(str).padStart(len, ch)
Why this matters
The failing test in step 2 wasn’t a flaw in padStart — it was a flaw in our refactor. The original module had two responsibilities: coerce the input to a string, and pad it. We replaced the second with a built-in but dropped the first. Re-adding the coercion takes one function call, brings every test back to green, and produces the punch line of the lecture: the eleven-line module that broke the internet is now a one-liner.
🎯 You will learn to
- Apply
String(value)as an explicit coercion idiom - Synthesize a contract-preserving refactor that keeps the entire test suite green
- Evaluate the trade-off between micro-dependencies and standard-library evolution
The fix
function leftPad (str, len, ch) {
return String(str).padStart(len, ch);
}
String(value) — called without new — is the standard explicit-coercion idiom. It is identical in result to the original module’s str = String(str) line. With the coercion in place, leftPad(7, 4, '0') runs as String(7).padStart(4, '0') → '7'.padStart(4, '0') → '0007'. All six tests pass.
The before/after, side by side
// BEFORE — the famous eleven lines.
function leftPad (str, len, ch) {
str = String(str);
var i = -1;
if (!ch && ch !== 0) ch = ' ';
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}
// AFTER — one line of ES2017.
function leftPad (str, len, ch) {
return String(str).padStart(len, ch);
}
✏️ Predict before you run
Does the one-line version also default the pad character to ' ' when ch is undefined?
- (a) No —
padStart(len, undefined)pads with'undefined'repeated. - (b) No —
padStart(len, undefined)throwsTypeError. - (c) Yes — the spec for
padStartsays “ifpadStringis undefined, use a single space.” - (d) Yes — but only because we still call
String(str), which secretly handles it.
Commit to a letter, then run.
Reveal (after running)
**(c)**. The ECMAScript spec for `String.prototype.padStart` explicitly defaults `padString` to `' '` when it's `undefined`. That's why the first test (`pads with spaces by default`) passes without us writing `if (!ch && ch !== 0) ch = ' '`. The built-in is doing the work the original module did manually. (a) is what you'd get if you *explicitly* passed `'undefined'`. (b) only happens with `null`-coerced edge cases far outside the contract.The real-world lesson
- The standard library evolves. Three lines you write today may become one built-in five years from now. Watch the spec.
- Micro-dependencies have a cost. Every
npm installis a supply-chain decision. If you can replace a dependency with a one-line built-in, you reduce maintenance, attack surface, and bundle size. - Read the spec, not just the docs.
padStart’s default-' 'rule is in TC39; without it, the one-liner wouldn’t preserve the contract.
Task
- In
leftpad.js, replace the body ofleftPadwithreturn String(str).padStart(len, ch); - Click ▶ Run.
- Confirm all six tests pass.
- Look at
leftpad.jsonce more — that’s the entire module. Sit with that.
// 🛠️ TASK: Replace the body of leftPad with:
//
// return String(str).padStart(len, ch);
//
// Then click ▶ Run. All six tests should pass.
function leftPad (str, len, ch) {
return str.padStart(len, ch); // still the naive swap — fix me!
}
module.exports = leftPad;
// Same contract tests as steps 1 and 2 — unchanged.
// The point is to watch the same suite confirm correctness
// across three different implementations.
const assert = require('node:assert');
const leftPad = require('./leftpad');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(' ✓ ' + name);
passed++;
} catch (e) {
console.log(' ✗ ' + name);
console.log(' ' + e.message);
failed++;
}
}
console.log('Running leftPad contract tests:\n');
test('pads with spaces by default', () => {
assert.strictEqual(leftPad('abc', 5), ' abc');
});
test('pads with a custom character', () => {
assert.strictEqual(leftPad('abc', 5, '0'), '00abc');
});
test('returns the string unchanged when length is already met', () => {
assert.strictEqual(leftPad('hello', 5, '0'), 'hello');
});
test('returns the string unchanged when target length is shorter', () => {
assert.strictEqual(leftPad('hello', 3, '0'), 'hello');
});
test('handles single-character padding to a wider target', () => {
assert.strictEqual(leftPad('x', 4, '*'), '***x');
});
test('accepts a number as input (coerces to string)', () => {
assert.strictEqual(leftPad(7, 4, '0'), '0007');
});
console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed > 0 ? 1 : 0);
Step 3 — Knowledge Check
Min. score: 80%
1. Why does the lecture write String(str) instead of new String(str)?
String(value) (no new) is the canonical explicit-coercion idiom — it returns a primitive string. new String(value) is a constructor call that wraps the primitive in a boxed String object; that object has all the prototype methods but fails === against primitives and behaves surprisingly in boolean contexts (new String('') is truthy). Almost every JavaScript style guide recommends String(value) for this reason.
2. (Spaced review — step 1) What does leftPad('hello', 3, '0') return under all three implementations the lecture walks through?
The “no truncation when already long enough” rule is a core part of the leftPad contract — and conveniently, String.prototype.padStart follows the same rule. That alignment is why padStart is a drop-in candidate at all. If padStart truncated, the standard library wouldn’t be a viable replacement.
3. (Spaced review — step 2) Five of the six tests passed under the naive return str.padStart(len, ch) swap. The lecture argues this still makes padStart a legitimate replacement candidate. Which of these is the strongest reason?
The single failing test isolates one piece of glue. The hard parts of the contract — defaulting the pad character, not truncating, repeating the pad to fill the gap — are absorbed by the spec-compliant built-in for free. That’s what makes padStart a true replacement: minimal glue, maximal correctness inherited from the standard library.