EduFlow
JavaScript Fundamentals
0%

Section 3 · Working with Data

Missing Values, Map and Set

17 min3 min readCode playground

Lesson at a glance

Read possibly-missing data safely with optional chaining and nullish coalescing, and choose Map for lookups and counts and Set for unique values.

  1. 1The error you will see most
  2. 2Optional chaining: ?.
  3. 3Defaults with ??
  4. 4Map: lookups with any key
  5. 5Set: each value once

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

Real data has holes. Some expenses have a note and some do not. Some have a receipt attached. A setting the user never changed is not saved at all. Pocket must handle all of these without crashing, and without mistaking “nothing” for a real value such as zero.

The error you will see most

Reading a property of an object that is not there is fine; you get undefined. Reading a property of undefined is not:

JavaScript
const expense = { label: "Coffee", amount: 450 };
console.log(expense.receipt);          // undefined: fine

try {
  console.log(expense.receipt.url);    // reading .url of undefined
} catch (error) {
  console.log(error.message);
}

TypeError: Cannot read properties of undefined (reading 'url') is probably the most common error in all of JavaScript. (The try/catch here only lets the example keep running so you can read the message; section 5 explains it properly.) The message tells you exactly what happened: something before .url was undefined.

Before reaching for a fix, remember the difference between the two “empty” values. undefined usually means “never set”: a missing property, a parameter nobody passed, a variable without a value. null is set on purpose to mean “deliberately empty”. Pocket might store receipt: null for an expense where the user chose not to attach one.

Optional chaining: ?.

The ?. operator reads a property only if the thing on its left is not null or undefined. If it is, the whole expression stops and gives undefined instead of throwing:

JavaScript
const withReceipt = { label: "Train", receipt: { url: "/r/88.png" } };
const withoutReceipt = { label: "Coffee" };

console.log(withReceipt.receipt?.url);
console.log(withoutReceipt.receipt?.url);
console.log(withoutReceipt.tags?.[0]);          // works with brackets
console.log(withoutReceipt.format?.());         // and with calls

Put ?. right after the part that may be missing. In expense.receipt?.url, you are saying “expense must exist, receipt might not”.

Defaults with ??

?. gives you undefined, and usually you want a fallback value. You met || for defaults in section 2, along with its flaw: it falls back for every falsy value, including 0 and "". The nullish coalescing operator ?? falls back only for null and undefined:

JavaScript
const settings = { dailyLimit: 0, nickname: "" };

console.log(settings.dailyLimit || 5000);   // 5000: the user's 0 is lost
console.log(settings.dailyLimit ?? 5000);   // 0: kept
console.log(settings.nickname ?? "friend"); // "": kept
console.log(settings.currency ?? "USD");    // missing: default used

Use ?? for defaults. Use || only when you really do want empty strings and zeros replaced too. They combine naturally with ?.:

JavaScript
const expense = { label: "Coffee" };
const receiptUrl = expense.receipt?.url ?? "No receipt";
console.log(receiptUrl);

There is also an assignment form, settings.currency ??= "USD", which sets the property only if it is currently null or undefined.

Map: lookups with any key

In the last lesson you used a plain object to total categories. Objects work well for records with known property names. For a collection that grows with data, keyed by values you do not know in advance, JavaScript has a dedicated structure, Map:

JavaScript
const counts = new Map();
for (const category of ["food", "travel", "food", "fun", "food"]) {
  counts.set(category, (counts.get(category) ?? 0) + 1);
}

console.log(counts.get("food"), counts.has("rent"), counts.size);
for (const [category, count] of counts) {
  console.log(`${category}: ${count}`);
}

set(key, value) stores, get(key) reads (or gives undefined), has(key) checks, delete(key) removes and size counts entries. A for...of loop gives you [key, value] pairs in the order they were first added.

Why not always use an object? A Map accepts any value as a key, including numbers and objects, where an object turns every key into a string. It has a real size. And it has no inherited properties to trip over: an object keyed by user-typed text can collide with built-in names such as constructor. A good default: objects for records with fixed fields, Maps for lookup tables built from data.

Set: each value once

A Set is a collection where every value appears at most once. Adding a value that is already there does nothing:

JavaScript
const categories = ["food", "travel", "food", "fun", "travel"];
const unique = new Set(categories);

console.log(unique.size, unique.has("fun"));
unique.add("food");               // already there: no change
console.log([...unique]);         // back to an array, in first-seen order

[...new Set(list)] is the standard one-line way to remove duplicates from an array. has is also much faster than includes on a large array, because a Set does not search item by item. Recent versions of JavaScript added set operations such as union and intersection, available in all current browsers.

That completes Pocket’s data toolkit: arrays, objects, Maps and Sets, and the operators for missing values. In section 4, all of it goes onto a real web page.

Key takeaways

  • Reading a property of undefined or null throws a TypeError; ?. stops and returns undefined instead.
  • ?? supplies a default only for null and undefined, so real values like 0 and "" survive, unlike with ||.
  • Use ?. only where data is genuinely optional; spreading it everywhere hides real bugs.
  • A Map stores key-value pairs with any type of key, keeps insertion order, and has get, set, has and size.
  • A Set stores each value at most once, so [...new Set(list)] removes duplicates.

Further reading

Check your understanding

4 questions

Question 1 of 4

An expense may or may not have a receipt object. Which expression reads the receipt’s url without ever throwing?

Question 2 of 4

A user’s saved tip percentage is 0. What does const tip = saved.tip ?? 15; give?

Question 3 of 4

You need to count how many expenses each category has, and later list categories in the order they first appeared. Which fits best?

Question 4 of 4

What does new Set(["food", "travel", "food", "fun"]).size return?

Practice

Code playground

Handle missing data safely

Some of Pocket’s data is incomplete. Fill in four functions:

  • receiptLabel(expense) returns the receipt’s url, or "No receipt" when receipt is missing or null.
  • dailyLimit(settings) returns settings.dailyLimit, or 5000 when there is no limit. settings itself may be undefined, and a saved limit of 0 must be kept.
  • countByCategory(expenses) returns a Map from category to the number of expenses in it. Expenses without a category count as "other".
  • uniqueCategories(expenses) returns an array with each category once, in the order first seen (missing ones as "other").
const expenses = [
  { label: "Coffee", amount: 450, category: "food" },
  { label: "Train", amount: 1220, category: "travel", receipt: { url: "/r/88.png" } },
  { label: "Lunch", amount: 1350, category: "food", receipt: null },
  { label: "Gift", amount: 5000 },
];

// The receipt's url, or "No receipt" (receipt may be missing or null)
function receiptLabel(expense) {
  return expense.receipt.url;
}

// settings may be undefined; a saved limit of 0 is a real choice
function dailyLimit(settings) {
  return settings.dailyLimit || 5000;
}

// A Map from category to how many expenses have it;
// expenses without a category count as "other"
function countByCategory(expenses) {
}

// Each category once, in the order first seen (missing ones as "other")
function uniqueCategories(expenses) {
  return expenses.map((e) => e.category);
}

console.log(receiptLabel(expenses[1]), dailyLimit({ dailyLimit: 0 }));
Loading the editor…

Ctrl/⌘ + Enter to run · Runs in a sandbox in your browser. Nothing leaves your device.

Console

(no output)

Checks

  • receiptLabel returns the url or “No receipt”
  • dailyLimit keeps 0, defaults to 5000, and survives missing settings
  • countByCategory returns a Map of counts, with “other” for missing categories
  • uniqueCategories lists each category once, in first-seen order