null, undefined and Symbols

Mental model: undefined is absence that happened to you; null is absence you chose.

Level: beginner · about 9 minutes

let declared;                     // never assigned
const obj = {};
function noReturn() {}

console.log(declared);            // undefined
console.log(obj.missing);         // undefined
console.log(noReturn());          // undefined
console.log([1, 2][9]);           // undefined

const cleared = null;             // you wrote this on purpose
console.log(cleared);             // null

Four ways to meet undefined, and only one way to meet null.

Both mean "no value", and the difference is intent. undefined is what the language hands you when something was never set. null is what you assign to say "there is deliberately nothing here". Nothing in the engine enforces that convention, but every codebase reads better when you follow it.

undefinednull
Who produces itthe languageyou
typeof'undefined''object', the 1995 bug
Triggers a default parameteryesno
JSON.stringify({ a: value }){} (the key is dropped){"a":null}
x ?? fallbackuses the fallbackuses the fallback
Number(x)NaN0

Default parameters only fire for undefined

function greet(name = 'friend') {
  return `Hello, ${name}`;
}

console.log(greet());          // 'Hello, friend'
console.log(greet(undefined)); // 'Hello, friend'
console.log(greet(null));      // 'Hello, null'
console.log(greet(''));        // 'Hello, ' (empty is a value)

?? and ?. treat them as a pair

const settings = { volume: 0, label: '', theme: null };

console.log(settings.volume ?? 5);  // 0, only null and undefined fall through
console.log(settings.volume || 5);  // 5, every falsy value falls through
console.log(settings.theme ?? 'dark');   // 'dark'
console.log(settings.missing?.nested);   // undefined, no error

Symbols: keys that cannot collide

A symbol is a primitive whose only feature is uniqueness. Every call to Symbol() produces a value equal to nothing else, ever, so you can add data to an object you do not own without any chance of overwriting a key that already exists or that someone adds later.

const ID = Symbol('id');            // the text is only a label for debugging
console.log(Symbol('id') === Symbol('id')); // false, always

const user = { name: 'Ada' };
user[ID] = 42;

console.log(user[ID]);              // 42
console.log(Object.keys(user));     // ['name'], the symbol is skipped
console.log(JSON.stringify(user));  // {"name":"Ada"}
console.log(Object.getOwnPropertySymbols(user).length); // 1
  • Symbol keys are skipped by Object.keys, for...in, JSON.stringify and spread of string keys.
  • They are not private: Object.getOwnPropertySymbols and Reflect.ownKeys will find them.
  • Symbol.for("key") uses a global registry, so the same text returns the same symbol across files.
  • A symbol cannot be coerced into a string implicitly. Use String(sym) or sym.description.

A preview: well-known symbols

The language keeps a set of built-in symbols that act as hooks into its own behaviour. You will meet them properly later; for now, notice that for...of is just a protocol you can implement.

const countdown = {
  from: 3,
  [Symbol.iterator]() {
    let n = this.from;
    return { next: () => (n > 0 ? { value: n--, done: false } : { done: true }) };
  },
};

console.log([...countdown]);        // [3, 2, 1]
`Symbol.iterator`
makes an object work with for...of and spread
`Symbol.asyncIterator`
the same, for for await...of
`Symbol.toPrimitive`
controls how the object converts to a number or a string
`Symbol.toStringTag`
changes what Object.prototype.toString reports
function greet(name = 'friend') {
  return `Hello, ${name}`;
}

console.log(greet(undefined), '|', greet(null));

A default parameter fires only when the argument is undefined. null is a real value, so it is passed through and interpolated as the text "null". This is the single most useful fact in this lesson.

Try it yourself

Absence, compared

const fromApi = { name: 'Ada', nickname: null };

function label(nickname = 'none') {
  return `nickname: ${nickname}`;
}

console.log(label(fromApi.nickname));              // 'nickname: null'
console.log(label(fromApi.nickname ?? undefined)); // 'nickname: none'
console.log(JSON.stringify({ a: undefined, b: null }));  // {"b":null}
console.log('missing' in fromApi, fromApi.missing);      // false undefined

Add a volume: 0 field and try both ?? and || on it. Which one gives you the answer you actually wanted?

Exercises

Hide metadata behind a symbol

Using the symbol OWNER, write tag(obj, name) which records name on obj under that symbol key and returns the same object, and ownerOf(obj) which reads it back. The tag must not show up in Object.keys or JSON.stringify.

Check yourself

What does this log?
'fallback' 0 'fallback' — ?? only falls through for null and undefined, so 0 survives it. || falls through for every falsy value, so 0 becomes the fallback. That third result is the bug that eats legitimate zeroes in real code.
What does a Symbol guarantee?
The key is unique, so it cannot collide with any other key — Uniqueness is the whole feature. It is not privacy: Object.getOwnPropertySymbols and Reflect.ownKeys list symbol keys. For real privacy use a closure or a #private class field.
Which of these will show you a symbol-keyed property?
Object.getOwnPropertySymbols — The first three all skip symbol keys, which is what makes symbols useful for metadata. Object.getOwnPropertySymbols and Reflect.ownKeys exist precisely so the properties are still discoverable when you need them.

Common mistakes

  • Passing null into a function with a default parameter and expecting the default to fire. Only undefined triggers it.
  • Using || for defaults, which throws away a legitimate 0, "" or false. Use ??.
  • Treating symbol keys as private. They are hidden from enumeration, not from inspection.

Takeaways

  • undefined is absence produced by the language; null is absence you chose.
  • Default parameters fire for undefined only, never for null.
  • ?? falls back for null and undefined only; || falls back for anything falsy.
  • Symbols are unique keys for metadata: skipped by enumeration, but not private.