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 beundefined
instead ofwindow
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:
- bound to the global object (
window
) in sloppy mode. undefined
in strict mode.
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 thethis
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 tothis
through that variable. By convention, the name of that variable isthat
: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:
- "Uncaught TypeError: Illegal invocation" in Chrome
- Why are certain function calls termed "illegal invocations" in JavaScript?
- Uncaught TypeError: Illegal invocation in JavaScript
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.