The 0012 Lookup: When Loose Equality Picks Two Rows
An internal order tool reads the query string ?sku=0012&qty=3 and looks up the matching catalog row. Catalog data arrives from a legacy inventory API where sku is a number, so SKU 0012 is stored as 12. The lookup helper is const row = catalog.find(item => item.sku == requestedSku), where requestedSku comes straight from URLSearchParams.get() and is therefore a string. Testers report that searching for 12 highlights two rows (the one for 12 and the one for 0012) and that changing the quantity on the second row silently updates the first. A second bug appears at checkout: with three units already in the cart and four more entered in the quantity box, the subtotal shows 34 instead of 7. The codebase compares values with == in 14 places, has no automated tests, and the team wants the smallest safe change before Friday's release. Node 18 and evergreen browsers are the only targets.
1. Explain precisely why `item.sku == requestedSku` matches both rows, and state what `'0012' == 12` evaluates to and why.
Read the worked answer
The == operator applies the Abstract Equality Comparison. When the operands have different types and one is a number, the other is converted with ToNumber, so '0012' == 12 becomes Number('0012') == 12, i.e. 12 == 12, which is true. The same happens for '12' == 12. Because find returns the first match only, the code that collects matches with filter lights up both rows, and the code that later writes quantity by SKU picks the wrong row. Leading zeros are not significant to Number, while a string-to-string comparison would keep them significant. === would return false for both pairs because the types differ, which surfaces the real defect: the lookup key is never normalized. Convert once at the boundary - const sku = Number(params.get('sku')) - validate with Number.isInteger, and compare numbers with ===, or keep SKUs as strings end to end.
2. The subtotal `34` bug: identify the coercion and give a fix that also rejects non-numeric input.
Read the worked answer
input.value is always a string, and + with a string operand performs concatenation: ToPrimitive on both sides, then if either result is a string, concatenate. So 3 + '4' is '34', not 7. The fix is to convert before arithmetic and validate: const qty = Number(input.value.trim()); if (input.value.trim() === '' || !Number.isInteger(qty) || qty <= 0) { showError(); return; } total += qty;. Prefer Number() over parseInt() because parseInt('4abc') silently returns 4; Number('4abc') returns NaN. But Number('') is 0, so an empty field would pass an integer check and add zero, hence the explicit empty-string test. Also check where total came from: if it was initialized from the DOM as '0', the concatenation chain restarts on the first addition. Keep the conversion at the edge and the arithmetic numeric internally.
3. A teammate proposes replacing every `==` with `===`. Where is that risky and what is your recommendation?
Read the worked answer
Blanket replacement is mostly right but not universally safe. x == null is the concise idiom for 'null or undefined'; changing it to === null silently stops catching undefined, which matters if discount is sometimes absent. Either keep == null deliberately and comment it, or write x === null || x === undefined, or use x ?? fallback. There are also comparisons where a number/string mismatch is the actual intent, such as comparing a selectedIndex to a string constant; those need one side converted, not the operator swapped, otherwise the condition becomes permanently false and the bug goes quiet instead of loud. Recommended path: enable eqeqeq with the null exception, normalize external input (URL, localStorage, dataset, form values) at the boundary, then apply === to the 14 sites. With no tests, add a couple of assertions around findItem before shipping.