Skip to content

TypeError: "x" is not a function — Causes and Fixes

TypeError: x is not a function fires when you call a value that isn't callable — a typo, a lost `this`, or the wrong shape from an API. Here's how to fix each case.

JavaScript javascript-errors typeerror nodejs this-binding v8 es-modules
Bharath G
Reading Progress

On This Page

The Error

In the browser console (Chrome / Edge, V8):

Uncaught TypeError: document.getElementByID is not a function
    at app.js:3:10

In Node.js, the same family of error looks like this — a real trace from a lost this inside a setTimeout callback, captured on Node.js v22.22.2:

/tmp/repro/repro.js:6
  console.log(`${this.name} says: ${this.speak()}`);
                                         ^

TypeError: this.speak is not a function
    at Dog.bark [as _onTimeout] (/tmp/repro/repro.js:6:42)
    at listOnTimeout (node:internal/timers:585:17)
    at process.processTimers (node:internal/timers:521:7)

Node.js v22.22.2

And from destructuring a name that was never exported:

/tmp/repro/repro3-main.js:3
console.log(parseDate("2026-09-11"));
            ^

TypeError: parseDate is not a function
    at Object.<anonymous> (/tmp/repro/repro3-main.js:3:13)
    at Module._compile (node:internal/modules/cjs/loader:1705:14)
    at Object..js (node:internal/modules/cjs/loader:1838:10)
    at Module.load (node:internal/modules/cjs/loader:1441:32)
    at Function._load (node:internal/modules/cjs/loader:1263:12)
    at TracingChannel.traceSync (node:diagnostics_channel:328:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:237:24)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
    at node:internal/main/run_main_module:36:49

Node.js v26.8.2

Per MDN's Not_a_function reference, this is one of the rare cases where the wording is unified across engines: Chrome/V8, Firefox/SpiderMonkey, and Safari/JavaScriptCore all print TypeError: "x" is not a function, where x is whatever expression you tried to call — a property path (obj.map), a bare identifier (parseDate), or a literal (2 if you write 2(3 + 5) and meant 2 * (3 + 5)). That consistency is the opposite of Cannot read properties of undefined, whose wording still diverges by engine — see the companion article on that error for the per-engine breakdown.

The message tells you three things at once: the expression that was evaluated, that it produced a value, and that the value has no internal [[Call]] method — it isn't a function object, so the engine can't invoke it. Everything below is about figuring out why that value wasn't a function.

How to Reproduce It

Case 1: losing this in a callback (Node.js, CommonJS)

javascript

// repro.js — CommonJS, Node.js
function Dog(name) {
  this.name = name;
}

Dog.prototype.bark = function () {
  console.log(`${this.name} says: ${this.speak()}`);
};

Dog.prototype.speak = function () {
  return "Woof";
};

const rex = new Dog("Rex");
setTimeout(rex.bark, 10); // passes the *function*, not a bound method

Run with node repro.js. rex.bark is detached from rex the instant it's passed as a plain reference — setTimeout calls it later with its own receiver. In Node's timer internals the callback runs as Dog.bark [as _onTimeout] (V8 infers that alias from the internal slot the callback is stored in, which is why the trace names it _onTimeout even though the function is really bark), and this inside the call is the Timeout object, not rex. this.speak doesn't exist on a Timeout, so the call throws. In a browser, the same pattern (setTimeout(rex.bark, 10)) throws too, but this resolves to window instead, since the HTML timer spec binds callback timers to the global object.

Case 2: wrong shape from an API response (Node.js or browser)

javascript

// repro2.js — works with Node's built-in fetch, or in a browser as-is
async function fetchUsers() {
  // Simulates an API that wraps results in an envelope: { data: [...] }
  return { data: [{ id: 1 }, { id: 2 }] };
}

async function main() {
  const users = await fetchUsers();
  const ids = users.map((u) => u.id); // users is the envelope, not the array
  console.log(ids);
}

main();

node repro2.js throws TypeError: users.map is not a functionusers is a plain object with a .data property, and plain objects don't have .map. This is the single most common production trigger: an API contract changed, added pagination, or wrapped the payload, and the calling code still assumes a bare array.

Case 3: destructuring an export that was never there

javascript

// repro3.js
module.exports = {
  formatDate(d) {
    return d.toISOString();
  },
};

javascript

// repro3-main.js
const { formatDate, parseDate } = require("./repro3.js");
console.log(formatDate(new Date())); // fine
console.log(parseDate("2026-09-11")); // parseDate was never exported

parseDate destructures to undefined — no error yet, destructuring a missing key is not itself a failure — and only throws once you try to call it. This is the shape you'll also hit with a default-vs-named import mismatch across CommonJS/ESM interop (const foo = require('pkg') when the package only has exports.foo); that specific interop failure mode gets its own full treatment in a dedicated CJS↔ESM article, so treat it here only as one more instance of "the value isn't what you expected."

Case 4: the classic typo (browser)

html

<!doctype html>
<html>
  <body>
    <button id="save">Save</button>
    <script>
      const el = document.getElementByID("save"); // capital ID, not Id
      el.addEventListener("click", () => console.log("saved"));
    </script>
  </body>
</html>

Open with a local server (npx serve .) or directly as file:// — this particular failure doesn't depend on CORS. DevTools prints Uncaught TypeError: document.getElementByID is not a function in Chrome, Firefox, and Safari alike.

Useful flags while chasing any of these: --stack-trace-limit isn't a real Node flag, but node --stack-size=<n> and DevTools' "Pause on exceptions" (set to "Uncaught Exceptions" at minimum, "All Exceptions" to catch it inside a try/catch that's silently swallowing it) are. For Node, node --trace-uncaught repro.js prints the point where the exception was created, which matters once the throw and the catch are in different async contexts.

Version / Environment Behaviour Matrix

This is a language-level error, not a runtime feature — it isn't gated by a Node or browser version. What is worth checking per engine is the message wording and, in Node, LTS status if your repro depends on a specific timers or module-loader trace shape.

Engine / runtimeWordingNotes
Chrome / Edge (V8)TypeError: "x" is not a functionProperty-path form: obj.foo is not a function. Stable since early V8.
Firefox (SpiderMonkey)TypeError: "x" is not a functionIdentical wording to V8, per MDN.
Safari (JavaScriptCore)TypeError: "x" is not a functionAlso unified; older WebKit builds sometimes said not a function. (In 'x()', 'x' is ...) for the related "not a constructor"/undefined-call cases, but the plain "is not a function" string has been stable for years.
Node.js 22 (Maintenance LTS, EOL 2027‑04‑30)Same as V8Timer-callback this behavior shown above is unchanged since early Node 4.x.
Node.js 24 (Active LTS, EOL 2028‑04‑30)Same as V8No change to this error path.
Node.js 26 (Current since 2026‑05‑05, V8 14.6)Same as V8Temporal shipping by default doesn't touch this error family; module-loader stack frames (node:internal/modules/cjs/loader) are unchanged in shape from 22/24.

What changes next: nothing engine-specific is scheduled to change this message. The one relevant shift is process-level: Node.js is moving to one major release per year starting with Node 27 (Alpha from October 2026, 27.0.0 in April 2027, LTS that October) — see the release-schedule announcement — so version-matrix tables like this one will simplify to a single yearly line going forward.

Related, if you're chasing the CJS/ESM interop variant of this error specifically: ERR_MODULE_NOT_FOUND and mandatory-extension resolution are covered in the sibling module-resolution articles in this series.

Why It Happens — Surface Level

JavaScript function calls don't check types at compile time (unless you're in TypeScript, and even then only for statically-known shapes). foo() just means "evaluate foo, then invoke whatever comes out." If that value isn't a function — it's undefined because a property doesn't exist, it's a plain object because an API changed shape, or it's a number because you forgot an operator — the engine can't invoke it and throws immediately at the call site, not at the point where the bad value was produced.

The four repros above are really one root cause wearing different hats: the value at the call site is not what the code assumed, and JavaScript's dynamic typing means nothing enforced that assumption between assignment and call.

Why It Happens — Under the Hood

A call expression f(...) compiles to a Call bytecode in V8's Ignition interpreter (or an equivalent op in SpiderMonkey/JSC). Before invoking, the engine checks whether the value implements the internal [[Call]] method — only Function objects (including classes, bound functions, and native functions) do. Ordinary objects, undefined, numbers, and arrays without a called-out method all lack [[Call]], so the check fails and a TypeError is thrown synchronously, before any bytecode inside the "function" runs.

Property lookups on the way to that call (obj.map, this.speak) go through V8's inline caches: the first time obj.map is read, V8 records obj's hidden class and where (or whether) map lives on it. If obj's hidden class has no map slot — because it's a plain object, not an Array.prototype-linked one — the load resolves to undefined, the IC records a miss, and the subsequent call throws. This is why swapping in the right type (an actual array, or an object with the right prototype chain) fixes the error without any code change at the call site: the hidden class lookup now resolves to a real function.

The this-loss case is about how JavaScript computes the receiver at call time, not compile time. rex.bark as an expression evaluates to a function value — the receiver information is not attached to the function, it's supplied fresh by however the function is eventually invoked. setTimeout(rex.bark, 10) hands the timers module a bare function reference; when the timer fires, Node's internal listOnTimeout invokes it as a method on the Timeout instance (callback.call(timeoutInstance, ...) under the hood), which is why our trace shows Dog.bark [as _onTimeout] — V8's stack-trace formatter names the frame using the function's own name (bark, inherited from the assignment Dog.prototype.bark = function () {...}) but annotates the call-site alias _onTimeout it was invoked through. Arrow functions sidestep this whole mechanism because they never had a this slot at the top level to begin with — they capture this lexically from the enclosing scope, once, at definition time.

The Fix

Case 1 — lost this: bind explicitly, or don't detach the method.

javascript

// Before
setTimeout(rex.bark, 10);

// After — bind
setTimeout(rex.bark.bind(rex), 10);

// After — arrow wrapper (also fine, and more common in modern code)
setTimeout(() => rex.bark(), 10);

Prefer arrow class fields when you own the class, so the method is pre-bound and safe to pass anywhere:

javascript

class Dog {
  constructor(name) {
    this.name = name;
  }
  speak = () => "Woof";
  bark = () => console.log(`${this.name} says: ${this.speak()}`);
}

Case 2 — wrong shape from an API: validate or unwrap at the boundary, don't guess deeper in the call stack.

javascript

// Before
const users = await fetchUsers();
const ids = users.map((u) => u.id);

// After — unwrap once, at the source
async function fetchUsers() {
  const res = await fetch("/api/users");
  const body = await res.json();
  return body.data; // return the array, not the envelope
}

If the shape is genuinely variable (some responses wrapped, some not), validate with a schema library (zod, valibot) right after the fetch, and throw a clear, typed error there instead of letting a confusing TypeError surface three functions later.

Case 3 — nonexistent export: this is a naming/typo bug at the source, not the call site — fix the export name, or check what the module actually exports with console.log(require("./repro3.js")) (CommonJS) or by reading the package's type declarations.

Where the fix belongs: for case 2 specifically, if you don't control the API, the fix belongs in your client-side adapter layer (normalize once, on the way in) — not scattered as defensive Array.isArray() checks at every call site downstream.

Best Practices & The Better Design

The fundamentally better fix is to stop the bad value from traveling far from where it's produced. Three practices do most of the work:

  • Validate at the boundary. Any value crossing a trust boundary — an HTTP response, JSON.parse output, a third-party callback payload — should be checked once, immediately, with a schema (zod.parse, valibot) or at minimum an explicit shape check, and turned into a typed error there rather than an opaque TypeError downstream.
  • Never pass an unbound method as a bare reference. obj.method used as a callback (array.forEach(obj.method), setTimeout(obj.method, n), emitter.on('event', obj.method)) is a footgun independent of this specific error — it silently changes this. Use arrow class fields, .bind(), or an explicit () => obj.method() wrapper every time a method crosses a callback boundary.
  • Let TypeScript catch the typo class of this bug entirely. document.getElementByID(...) is a compile error under strict TypeScript (Property 'getElementByID' does not exist on type 'Document'), because Document's DOM lib types don't have that member. Case 3 — destructuring a nonexistent export — is also a compile-time TS2339/TS2305 under strict, because the module's inferred or declared type doesn't have parseDate.

Rewritten "the right way," combining the binding fix and the validation fix, runs as written on Node.js 22/24/26:

javascript

// users.js
import { z } from "zod";

const UsersEnvelope = z.object({
  data: z.array(z.object({ id: z.number() })),
});

export async function fetchUsers() {
  const res = await fetch("/api/users");
  const parsed = UsersEnvelope.parse(await res.json());
  return parsed.data; // guaranteed to be an array, or parse() already threw
}

How to Prevent It Long-Term

  • TypeScript in strict mode (noImplicitAny, and ideally noUncheckedIndexedAccess) turns the typo and nonexistent-export cases into build failures instead of runtime crashes.
  • @typescript-eslint/no-unsafe-call and @typescript-eslint/no-unsafe-member-access flag calls on values typed (or inferred) as any, which is exactly the escape hatch that lets this error reach runtime in a typed codebase.
  • @typescript-eslint/unbound-method catches the "passed a method as a bare reference" pattern (Case 1) at lint time, before it ever reaches a timer or event emitter.
  • Schema validation at every external boundary (zod, valibot, or even a hand-written shape guard) turns "wrong shape from the API" into a clear, immediate, typed error at the fetch site instead of a confusing TypeError several function calls later.
  • Runtime signals worth watching: group production TypeError volume by message text in Sentry/Rollbar/Datadog — a spike in "* is not a function" errors immediately after a deploy is a strong signal of either an API contract change or a bundler/minifier issue (property mangling breaking a call). Correlate with deploy timestamps, not just error rate.
  • Tests that catch it: a contract test against the real API shape (not a hand-rolled mock that happens to match your assumptions) would have caught Case 2 before production; a bind/arrow-function convention enforced by lint, not memory, prevents Case 1 from recurring across a team.

Important

  • TypeError: "x" is not a function means the call-site value lacks [[Call]] — it's unified wording across Chrome, Firefox, and Safari, unlike the undefined/null property-access errors.
  • The four real-world triggers are a typo, a value with the wrong shape (often from an API), a lost this when a method is passed as a bare callback, and destructuring a name that was never exported.
  • Losing this is a call-time problem, not a compile-time one — obj.method as a plain reference forgets obj the instant it's passed somewhere else; arrow class fields or .bind() fix it permanently.
  • Validate external data once, at the boundary, with a schema — don't let a malformed API response travel three function calls before it turns into a confusing TypeError.
  • strict TypeScript plus @typescript-eslint/no-unsafe-call/unbound-method converts most of this error class from a production incident into a build failure.
JavaScriptjavascript-errorstypeerrornodejsthis-bindingv8es-modules

From aspiring developer to blogger, I test learning platforms, simplify programming syntax, and share resources that work. Helping you code smarter as I grow myself. New or experienced, you're welcome here.

Comments