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); // nullFour 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.
undefined | null | |
|---|---|---|
| Who produces it | the language | you |
typeof | 'undefined' | 'object', the 1995 bug |
| Triggers a default parameter | yes | no |
JSON.stringify({ a: value }) | {} (the key is dropped) | {"a":null} |
x ?? fallback | uses the fallback | uses the fallback |
Number(x) | NaN | 0 |
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.stringifyand spread of string keys. - They are not private:
Object.getOwnPropertySymbolsandReflect.ownKeyswill 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)orsym.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...ofand 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.toStringreports
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 fornullandundefined, so0survives it.||falls through for every falsy value, so0becomes the fallback. That third result is the bug that eats legitimate zeroes in real code. - What does a
Symbolguarantee? - The key is unique, so it cannot collide with any other key — Uniqueness is the whole feature. It is not privacy:
Object.getOwnPropertySymbolsandReflect.ownKeyslist symbol keys. For real privacy use a closure or a#privateclass 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.getOwnPropertySymbolsandReflect.ownKeysexist precisely so the properties are still discoverable when you need them.
Common mistakes
- Passing
nullinto a function with a default parameter and expecting the default to fire. Onlyundefinedtriggers it. - Using
||for defaults, which throws away a legitimate0,""orfalse. Use??. - Treating symbol keys as private. They are hidden from enumeration, not from inspection.
Takeaways
undefinedis absence produced by the language;nullis absence you chose.- Default parameters fire for
undefinedonly, never fornull. ??falls back fornullandundefinedonly;||falls back for anything falsy.- Symbols are unique keys for metadata: skipped by enumeration, but not private.