JavaScript

5 Common Bugs Caused by JS Primitives & How to Fix Them

Dive into the 5 most common JavaScript bugs caused by primitives like number, string, and null. Learn why they happen and how to write robust, error-free code.

M

Maria Petrova

Senior Frontend Engineer specializing in JavaScript performance, architecture, and robust application development.

7 min read12 views

5 Common Bugs Caused by JS Primitives & How to Fix Them

JavaScript is famous for its flexibility and ease of use, but this very nature can sometimes lead to perplexing bugs that leave even seasoned developers scratching their heads. Often, the culprit isn't a complex library or an intricate algorithm, but the fundamental building blocks of the language: its primitives. Understanding their quirks is key to writing robust, predictable code.

1. The `typeof null` Deception

This is a classic JavaScript "gotcha" that has been part of the language since its inception. It's a simple mistake to make when you're trying to check if a variable has been intentionally set to nothing.

The Problem: `null` is an 'object'?

You might intuitively write a check like this to handle a variable that could be an object or null:

function processData(data) {  if (typeof data === 'object') {    // This block will run for both objects AND null!    console.log('Processing data...');    // ... might lead to TypeError: Cannot read properties of null  } else {    console.log('No data to process.');  }}processData({ key: 'value' }); // => Processing data...processData(null);             // => Processing data... (Uh oh!)

The expression typeof null unexpectedly returns 'object'. This is a historical bug in JavaScript that, for backward compatibility reasons, will likely never be fixed. Relying on typeof to distinguish a real object from null will inevitably lead to errors.

The Fix: Be Explicit

The most robust way to check for null is with a strict equality check. To correctly handle this, you must first check for null before checking for the object type.

function processData(data) {  // First, check if data is null. Then, check its type.  if (data !== null && typeof data === 'object') {    console.log('Processing data...');    // Now this block is safe from null values.  } else {    console.log('No data to process.');  }}processData({ key: 'value' }); // => Processing data...processData(null);             // => No data to process. (Correct!)

Key takeaway: Always use value === null to check for null. Never rely on typeof for this purpose.

2. The Floating-Point Fiasco

If you've ever worked with money or precise calculations in JavaScript, you've likely run into this baffling issue. Simple arithmetic just doesn't seem to add up.

The Problem: Imprecise Math

Try this in your console:

console.log(0.1 + 0.2); // => 0.30000000000000004

Wait, what? The result isn't 0.3. This isn't a bug in JavaScript itself, but a consequence of how computers store floating-point numbers using the IEEE 754 standard. Binary representations of some decimal fractions are not exact, leading to these tiny precision errors that can compound and cause major issues in financial calculations or scientific applications.

0.1 + 0.2 === 0.3; // false!

The Fix: Avoid Floats for Precision

You have a few solid options to handle this correctly.

Advertisement

1. Work with Integers: For currency, the best practice is to perform all calculations in the smallest unit (e.g., cents) as integers. Integers are exact.

const price1InCents = 10; // $0.10const price2InCents = 20; // $0.20const totalInCents = price1InCents + price2InCents; // 30console.log(totalInCents / 100); // 0.3 (for display only)

2. Use `Number.EPSILON`: For comparisons, you can check if the difference between two numbers is smaller than a tiny value called `Number.EPSILON`.

function numbersAreClose(a, b) {  return Math.abs(a - b) < Number.EPSILON;}console.log(numbersAreClose(0.1 + 0.2, 0.3)); // true

3. Use a Library: For complex decimal math, it's often safest to use a battle-tested library like Decimal.js or Big.js.

3. The Coercion Trap: String vs. Number

JavaScript's automatic type coercion is a double-edged sword. It can make code more concise, but it can also introduce subtle bugs when you're not paying attention, especially when the + operator is involved.

The Problem: Addition or Concatenation?

The + operator is overloaded: it performs addition for numbers and concatenation for strings. If one of the operands is a string, JavaScript will convert the other to a string and concatenate them.

const valueFromInput = '5'; // Input values are always strings!const shippingCost = 10;const total = valueFromInput + shippingCost;console.log(total); // => '510' (Not 15!)

This is a common source of bugs when dealing with form inputs, API responses, or URL parameters, which are often delivered as strings.

The Fix: Explicit Type Conversion

Never rely on implicit coercion for arithmetic. Always explicitly convert your string values to numbers before performing calculations. You have several tools for this.

const valueFromInput = '5';const shippingCost = 10;// Option 1: Unary Plus (clean and concise)const total1 = +valueFromInput + shippingCost; // 15// Option 2: parseInt() (for integers)const total2 = parseInt(valueFromInput, 10) + shippingCost; // 15// Option 3: parseFloat() (for decimals)const valueWithDecimal = '5.99';const total3 = parseFloat(valueWithDecimal) + shippingCost; // 15.99

Here's a quick comparison of the common methods:

MethodDescriptionExampleResult
+strThe unary plus operator. A concise way to convert to a number.+'42.5'42.5
Number(str)The `Number` constructor. More explicit but can be verbose.Number('42.5')42.5
parseInt(str, 10)Parses a string and returns an integer. Stops at the first non-digit. Always use the radix 10.parseInt('42px', 10)42
parseFloat(str)Parses a string and returns a floating-point number.parseFloat('42.5em')42.5

4. The `NaN` Identity Crisis

NaN, which stands for "Not-a-Number," is a special value of the `number` primitive. It represents the result of an undefined or unrepresentable mathematical operation, like dividing by zero or trying to parse a non-numeric string.

The Problem: `NaN` is Not Equal to Itself

Here’s the most bizarre thing about NaN: it is the only value in JavaScript that is not equal to itself.

const result = 10 / 'apple';console.log(result); // => NaN// This check will ALWAYS fail!if (result === NaN) {  console.log('Calculation failed!'); // This code never runs}

This means you can't check for NaN using strict (===) or loose (==) equality. This can completely break your validation logic if you're not aware of it.

The Fix: Use `Number.isNaN()`

The modern, correct way to check for NaN is the ES6 method Number.isNaN(). It returns true only if the value is `NaN`, and `false` for everything else.

const result = 10 / 'apple';if (Number.isNaN(result)) {  console.log('Calculation failed!'); // This works!}

You might also see the global function isNaN(). Be careful with it! It coerces its argument to a number first, which can lead to unexpected results:

isNaN('hello');          // true, because 'hello' coerces to NaNNumber.isNaN('hello');   // false, because 'hello' is a string, not the value NaN

Unless you specifically want that coercive behavior, always prefer `Number.isNaN()` for its predictability.

5. The Falsy Value Pitfall

In JavaScript, certain values are considered "falsy," meaning they evaluate to `false` in a boolean context (like an `if` statement). The falsy primitives are: false, 0, `''` (empty string), null, `undefined`, and NaN. (Note: `BigInt` has `0n`).

The Problem: Valid Values Can Be Falsy

This becomes a problem when a falsy value is a valid, meaningful state in your application. A common example is a count that could be zero.

function displayItems(itemCount) {  // This check is too simple and therefore buggy  if (itemCount) {    console.log(`Displaying ${itemCount} items.`);  } else {    console.log('Please select the number of items.');  }}displayItems(5);  // => 'Displaying 5 items.' (Correct)displayItems(0);  // => 'Please select the number of items.' (Wrong!)

In the example above, `0` is a valid number of items, but because it's falsy, our function behaves as if the count was never provided.

The Fix: Be Explicit About What You're Checking

Don't use a generic truthy/falsy check when you need to distinguish between different falsy values. Instead, be explicit about the condition you're actually testing for.

If you want to ensure a value has been defined and is not `null` or `undefined`:

function displayItems(itemCount) {  if (itemCount !== null && itemCount !== undefined) {    console.log(`Displaying ${itemCount} items.`);  } else {    console.log('Please select the number of items.');  }}displayItems(5); // => 'Displaying 5 items.'displayItems(0); // => 'Displaying 0 items.' (Correct!)

If you specifically need to check for a number:

function displayItems(itemCount) {  if (typeof itemCount === 'number') {    console.log(`Displaying ${itemCount} items.`);  } else {    console.log('Please select the number of items.');  }}

This explicit check correctly handles `0` while filtering out `null`, `undefined`, and empty strings.

Final Thoughts

The primitives in JavaScript are powerful but full of historical quirks and subtle behaviors. Mastering them is a rite of passage for any developer. By remembering to be explicit with your checks, understanding the limits of number precision, and respecting the oddities of `null` and `NaN`, you can avoid these common pitfalls and write cleaner, more resilient, and bug-free code.

Tags

You May Also Like