"Illegal invocation" errors in JavaScript

Published on in JavaScript

Last updated on

The error is thrown when calling a function whose this keyword isn't referring to the object where it originally did, i.e. when the "context" of the function is lost.

Table of contents

Example problems

I encountered the "illegal invocation" error when calling the destructured abort method of an AbortController:

const abortController = new AbortController()
const { abort } = abortController

abort() //=> TypeError: Illegal invocation

Another case: trying to implement jQuery-like shorthands for document.querySelector and document.querySelectorAll:

const $ = document.querySelector
const $$ = document.querySelectorAll

$('#foo') //=> TypeError: Illegal invocation
$$('.bar') //=> TypeError: Illegal invocation

(By the way: most, if not all, modern browsers have the $ and $$ shorthands built into the browser's JS console.)

Description of the error

"Invocation" is the act invoking a function, which is the same as calling a function. Invoke = call.

An "illegal invocation" error is thrown when calling a function whose this keyword doesn't refer to the object where it originally did. In other words, the original "context" of the function is lost.

Chromium browsers call this error an "illegal invocation."

Firefox produces more descriptive error messages:

  • TypeError: 'abort' called on an object that does not implement interface AbortController.

  • TypeError: 'querySelector' called on an object that does not implement interface Document.

So does Safari:

  • TypeError: Can only call AbortController.abort on instances of AbortController

  • TypeError: Can only call Document.querySelector on instances of Document

Node.js (v16) produces clearly the best error messages, e.g.:

  • TypeError [ERR_INVALID_THIS]: Value of "this" must be of type AbortController

Deno uses V8 – the same JS engine as Chromium browsers do – so Deno also calls the error an "illegal invocation."

Manual implementation of an "illegal invocation" check

For demonstration purposes:

const foo = {
  bar: function () {
    console.log('Calling foo.bar; `this` refers to', this)

    if (this !== foo) {
      throw new TypeError('Illegal invocation 🛑')
    }

    console.log('Successfully called foo.bar ✅')
  },
}

foo.bar()
//=> Calling foo.bar; `this` refers to foo
//=> Successfully called foo.bar ✅

const { bar } = foo
bar()
//=> Calling foo.bar; `this` refers to window
//=> TypeError: Illegal invocation 🛑

const bar2 = foo.bar
bar2()
//=> Calling foo.bar; `this` refers to window
//=> TypeError: Illegal invocation 🛑

Notes:

  • In real code, you should use better error messages. "Illegal invocation" is not clear.
  • In strict mode, this would be undefined instead of window in the error cases.

Why does the this keyword change?

Because the this keyword in JavaScript is confusing!

The gist is in the difference between method invocations and function invocations.

Method invocations

A method is a function stored as a property of an object. When invoking (i.e. calling) a method using the dot notation or square bracket notation, the this keyword is bound to the object:

const foo = {
  bar() {
    console.log(this)
  },
}

const method = 'bar'

// Method invocations:
foo.bar() //=> foo
foo['bar']() //=> foo
foo[method]() //=> foo

Function invocations

When invoking (i.e. calling) a function that is not the property of an object, the this keyword is:

In either mode, the original context is lost because the this keyword doesn't refer to the object where it originally did:

const { bar } = foo
const bar2 = foo.bar
const bar3 = foo['bar']

// Function invocations:
bar() //=> window (in sloppy mode) / undefined (in strict mode)
bar2() //=> window (in sloppy mode) / undefined (in strict mode)
bar3() //=> window (in sloppy mode) / undefined (in strict mode)

As to why the context is lost – let's quote Douglas Crockford's book JavaScript: The Good Parts (1st ed., p. 28; emphasis added):

When a function is not the property of an object, then it is invoked as a function:

var sum = add(3, 4) // sum is 7

When a function is invoked with this pattern, this is bound to the global object. This was a mistake in the design of the language.

Sidetrack: arrow functions

The quote from the book continues (pp. 28–29; text split into paragraphs and code block slightly edited):

Had the language been designed correctly, when the inner function is invoked, this would still be bound to the this variable of the outer function.

A consequence of this error is that a method cannot employ an inner function to help it do its work because the inner function does not share the method's access to the object as its this is bound to the wrong value.

Fortunately, there is an easy workaround. If the method defines a variable and assigns it the value of this, the inner function will have access to this through that variable. By convention, the name of that variable is that:

var myObject = {
  value: 3,
}

// Augment myObject with a double method
myObject.double = function () {
  var that = this // Workaround

  var helper = function () {
    that.value = add(that.value, that.value)
  }

  helper() // Invoke helper as a function
}

myObject.double() // Invoke double as a method

console.log(myObject.value) //=> 6

Nowadays you can alternatively use arrow functions:

myObject.double = function () {
  const helper = () => {
    this.value = add(this.value, this.value)
  }

  helper() // Invoke helper as a function
}

myObject.double() // Invoke double as a method

console.log(myObject.value) //=> 6

Arrow functions increase the complexity around the this keyword; or reduce complexity, depending on the viewpoint.

Anyhow, the this keyword in JavaScript is confusing. I personally try to avoid it. It has many potential pitfalls, and often there are better alternatives.

Three ways to fix the error

Here's the original, problematic example code:

const abortController = new AbortController()
const { abort } = abortController

const $ = document.querySelector
const $$ = document.querySelectorAll

The gist of the problem is that calling abort, $ or $$ is a function invocation, not a method invocation, so the context is lost.

Create a function that calls a method

As we learned above, with a method invocation (as opposed to a function invocation), the this keyword is bound to the object.

So, create an abort function that calls the abortController.abort method:

const abortController = new AbortController()
const abort = () => abortController.abort()

abort() // OK!

Calling abort is a function invocation, but abort in turn calls abortController.abort using method invocation, so the context is not lost.

Similarly for $ and $$:

const $ = (selectors) => document.querySelector(selectors)
const $$ = (selectors) => document.querySelectorAll(selectors)

$('#foo') // OK!
$$('.bar') // OK!

(By the way: notice how the function parameters are in the plural form: selectors instead of selector. That's because document.querySelector and document.querySelectorAll accept a comma-separated list of CSS selectors.)

Use bind() to change the this keyword

A more convoluted solution is to use Function.prototype.bind() to set the this keyword to point to the correct object:

const abortController = new AbortController()
const abort = abortController.abort.bind(abortController)

const $ = document.querySelector.bind(document)
const $$ = document.querySelectorAll.bind(document)

abort() // OK!

$('#foo') // OK!
$$('.bar') // OK!

There's also Function.prototype.apply() and Function.prototype.call(), but they are also convoluted because they deal with the this keyword.

I recommend the previous solution which doesn't deal with the this parameter: Create a function that calls a method.

Export the whole object

(Maybe an obvious solution, but mentioning it anyway.)

In the AbortController case, I originally destructured the abort method because I wanted to export only that method, not the whole AbortController.

If you are fine with exporting the whole AbortController, calling its abort method directly is fine too (because it'll be a method invocation):

export const abortController = new AbortController()

// In another file:

import { abortController } from '...'
abortController.abort() // OK!

This solution doesn't apply to the $ and $$ functions because document is anyway available in all modules.

Sources / Further resources

I learned about "illegal invocation" errors via these Stack Overflow questions:

I learned about the differences between method invocations and function invocations from Douglas Crockford's book JavaScript: The Good Parts. Chapter 4, "Functions," has more details, and also describes two other invocation patterns in JavaScript:

  • the constructor invocation pattern
  • the apply invocation pattern.

Lastly, see also my theoretical musings about preventing "illegal invocation" errors in TypeScript.