undefined vs. null revisited

[2021-01-27] dev, javascript, jshistory
(Ad, please don’t block)

Many programming languages have one “non-value” called null. It indicates that a variable does not currently point to an object – for example, when it hasn’t been initialized yet.

In contrast, JavaScript has two such non-values: undefined and null. In this blog post, we examine how they differ and how to best use or avoid them.

undefined vs. null  

Both values are very similar and often used interchangeably. How they differ is therefore subtle.

The ECMAScript language specification on undefined vs. null  

The ECMAScript language specification describes them as follows:

  • undefined is “used when a variable has not been assigned a value” (source).
  • null “represents the intentional absence of any object value” (source).

We’ll see later how to best handle these two values as a programmer.

Two non-values – a mistake that can’t be removed  

Having two non-values in JavaScript is now considered a design mistake (even by JavaScript’s creator, Brendan Eich).

Why isn’t one of those values removed from JavaScript, then? One core principle of JavaScript is to never break backward compatibility. That principle has many upsides. Its biggest downside is that design mistakes can’t be removed.

The history of undefined and null  

In Java (which inspired many aspects of JavaScript), initialization values depend on the static type of a variable:

  • Variables with object types are initialized with null.
  • Each primitive type has its own initialization value. For example, int variables are initialized with 0.

In JavaScript, each variable can hold both object values and primitive values. Therefore, if null means “not an object”, JavaScript also needs an initialization value that means “neither an object nor a primitive value”. That initialization value is undefined.

Occurrences of undefined in the language  

If a variable myVar has not been initialized yet, its value is undefined:

let myVar;
assert.equal(myVar, undefined);

If a property .unknownProp is missing, accessing the property produces the values undefined:

const obj = {};
assert.equal(obj.unknownProp, undefined);

If a function does not explicitly return anything, the function implicitly returns undefined:

function myFunc() {}
assert.equal(myFunc(), undefined);

If a function has a return statement without an argument, the function implicitly returns undefined:

function myFunc() {
  return;
}
assert.equal(myFunc(), undefined);

If a parameter x is omitted, the language initializes that parameter with undefined:

function myFunc(x) {
  assert.equal(x, undefined);
}
myFunc();

Optional chaining via obj?.someProp returns undefined if obj is undefined or null:

> undefined?.someProp
undefined
> null?.someProp
undefined

Occurrences of null in the language  

The prototype of an object is either an object or, at the end of a chain of prototypes, null. Object.prototype does not have a prototype:

> Object.getPrototypeOf(Object.prototype)
null

If we match a regular expression (such as /a/) against a string (such as 'x'), we either get an object with matching data (if matching was successful) or null (if matching failed):

> /a/.exec('x')
null

The JSON data format does not support undefined, only null:

> JSON.stringify({a: undefined, b: null})
'{"b":null}'

Operators that treat undefined and/or null specially  

undefined and parameter default values  

A parameter default value is used if:

  • A parameter is missing.
  • A parameter has the value undefined.

For example:

function myFunc(arg='abc') {
  return arg;
}
assert.equal(myFunc('hello'), 'hello');
assert.equal(myFunc(), 'abc');
assert.equal(myFunc(undefined), 'abc');

That undefined also triggers the parameter default value points towards it being a metavalue.

The following example demonstrates where that is useful:

function concat(str1='', str2='') {
  return str1 + str2;
}
function twice(str) { // (A)
  return concat(str, str);
}

In line A, we don’t specify a parameter default value for str. When this parameter is missing, we forward that status to concat() and let it pick a default value.

undefined and destructuring default values  

Default values in destructuring work similarly to parameter default values – they are used if a variable either has no match in the data or if it matches undefined:

const [a='a'] = [];
assert.equal(a, 'a');

const [b='b'] = [undefined];
assert.equal(b, 'b');

const {prop: c='c'} = {};
assert.equal(c, 'c');

const {prop: d='d'} = {prop: undefined};
assert.equal(d, 'd');

undefined and null and optional chaining  

When there is optional chaining via value?.prop:

  • If value is undefined or null, return undefined. That is, this happens whenever value.prop would throw an exception.
  • Otherwise, return value.prop.
function getProp(value) {
  // optional static property access
  return value?.prop;
}
assert.equal(
  getProp({prop: 123}), 123);
assert.equal(
  getProp(undefined), undefined);
assert.equal(
  getProp(null), undefined);

The following two operations work similarly:

obj?.[«expr»] // optional dynamic property access
func?.(«arg0», «arg1») // optional function or method call

undefined and null and nullish coalescing  

The nullish coalescing operator ?? lets us use a default value if a value is undefined or null:

> undefined ?? 'default value'
'default value'
> null ?? 'default value'
'default value'

> 0 ?? 'default value'
0
> 123 ?? 'default value'
123
> '' ?? 'default value'
''
> 'abc' ?? 'default value'
'abc'

The nullish coalescing assignment operator ??= combines nullish coalescing with assignment:

function setName(obj) {
  obj.name ??= '(Unnamed)';
  return obj;
}
assert.deepEqual(
  setName({}),
  {name: '(Unnamed)'}
);
assert.deepEqual(
  setName({name: undefined}),
  {name: '(Unnamed)'}
);
assert.deepEqual(
  setName({name: null}),
  {name: '(Unnamed)'}
);
assert.deepEqual(
  setName({name: 'Jane'}),
  {name: 'Jane'}
);

Handling undefined and null  

The following subsections explain the most common ways of handling undefined and null in our own code.

Neither undefined nor null are used as actual values  

As an example, we may want a property file.title to always exist and to always be a string. There are two common ways to achieve this.

Note that, in this blog post, we only check for undefined and null and not whether a value is a string or not. You have to decide for yourself if you want to implement that as an additional security measure or not.

Both undefined and null are forbidden  

This looks as follows:

function createFile(title) {
  if (title === undefined || title === null) {
    throw new Error('`title` must not be nullish');
  }
  // ···
}

Why choose this approach?

  • We want to treat undefined and null the same because JavaScript code often does – for example:

    // Detecting if a property exists
    if (!obj.requiredProp) {
      obj.requiredProp = 123;
    }
    
    // Default values via nullish coalescing operator
    const myValue = myParameter ?? 'some default';
    
  • If there is an issue in our code and either undefined or null appears, we want it to fail as quickly as possible.

Both undefined and null trigger defaults  

This looks as follows:

function createFile(title) {
  title ??= '(Untitled)';
  // ···
}

We can’t use a parameter default value here because it is only triggered by undefined. Instead, we rely on the nullish coalescing assignment operator ??=.

Why choose this approach?

  • We want to treat undefined and null the same (see previous section).
  • We want our code to deal robustly and silently with undefined and null.

Either undefined or null is a “switched off” value  

As an example, we may want a property file.title to be either a string or “switched off” (file doesn’t have a title). There are several ways to achieve this.

null is the “switched off” value  

This looks as follows:

function createFile(title) {
  if (title === undefined) {
    throw new Error('`title` must not be undefined');
  }
  return {title};
}

Alternatively, undefined can trigger a default value:

function createFile(title = '(Untitled)') {
  return {title};
}

Why choose this approach?

  • We need a non-value that means “switched off”.
  • We don’t want our non-value to trigger parameter default values and destructuring default values.
  • We want to stringify the non-value as JSON (something that we can’t do with undefined).

undefined is the “switched off” value  

This looks as follows:

function createFile(title) {
  if (title === null) {
    throw new Error('`title` must not be null');
  }
  return {title};
}

Why choose this approach?

  • We need a non-value that means “switched off”.
  • We do want our non-value to trigger parameter default values and destructuring default values.

One downside of undefined is that it is often created accidentally in JavaScript: by an uninitialized variable, a typo in a property name, forgetting to return something from a function, etc.

Why not use both undefined and null as “switched off” values?  

When receiving a value, it can make sense to treat both undefined and null as “not a value”. However, when we are creating values, we want to be unambiguous so that handling those values remains simple.

This points toward a different approach: What if we need a “switched off” value, but don’t want to use either undefined or null as such a value? Read on for details.

Other ways of handling “switched off”  

Special value  

We can create a special value that we use whenever the property .title is switched off:

const UNTITLED = Symbol('UNTITLED');
const file = {
  title: UNTITLED,
};

Null object pattern  

The null object pattern comes from object oriented programming:

  • All subclasses of a common superclass have the same interface.
  • Each subclass implements a different mode in which an instance operates.
  • One of those modes is “null”.

In the following example, UntitledFile implements the “null” mode.

// Abstract superclass
class File {
  constructor(content) {
    if (new.target === File) {
      throw new Error('Can’t instantiate this class');
    }
    this.content = content;
  }
}

class TitledFile extends File {
  constructor(content, title) {
    super(content);
    this.title = title;
  }
  getTitle() {
    return this.title;
  }
}

class UntitledFile extends File {
  constructor(content) {
    super(content);
  }
  getTitle() {
    return '(Untitled)';
  }
}

const files = [
  new TitledFile('Dear diary!', 'My Diary'),
  new UntitledFile('Reminder: pick a title!'),
];

assert.deepEqual(
  files.map(f => f.getTitle()),
  [
    'My Diary',
    '(Untitled)',
  ]);

We also could have used the null object pattern for just the title (instead of for the whole file object).

Maybe type  

The Maybe type is a function programming technique:

function getTitle(file) {
  switch (file.title.kind) {
    case 'just':
      return file.title.value;
    case 'nothing':
      return '(Untitled)';
    default:
      throw new Error();
  }
}

const files = [
  {
    title: {kind: 'just', value: 'My Diary'},
    content: 'Dear diary!',
  },
  {
    title: {kind: 'nothing'},
    content: 'Reminder: pick a title!',
  },
];

assert.deepEqual(
  files.map(f => getTitle(f)),
  [
    'My Diary',
    '(Untitled)',
  ]);

We could have encoded “just” and “nothing” via Arrays. The benefit of our approach is that it is well supported by TypeScript (via discriminating unions).

My approach  

There are three reasons why I don’t like to use undefined as a “switched off” value:

  • undefined often appears accidentally in JavaScript.
  • undefined triggers default values for parameters and destructuring (some people prefer undefined for the same reason).

Therefore I use either of the following two approaches if I need a special value:

  • I use null as a “switched off” value. (As an aside, this approach is relatively well supported by TypeScript.)
  • I avoid both undefined and null via one of the techniques described above. This has the upside of being cleaner and the downside of involving more work.

More content on JavaScript: My book “JavaScript for impatient programmers” is free to read online! This is its sales pitch:

This book makes JavaScript less challenging to learn for newcomers, by offering a modern view that is as consistent as possible.