~/troubleshooting/javascript-typeerror-x-is-not-a-function-5-root-causes
§ POST · MAY 13, 2026 v1.0

JavaScript TypeError: X is not a function – 5 root causes

Five root causes of TypeError: X is not a function in JavaScript, ranked by how often I see them in production logs, with the one-line fix for each.
Ryan CallowayStaff contributor
  8 min read

By Ryan Calloway. Updated May 2026.

A 2021 r/learnjavascript thread opens with the symptom you have probably hit at least once: Geolocation.getCurrentPosition() works, Geolocation.watchPosition() throws callback is not a function. The top reply nails it: the OP wrapped watchPosition as if it returned a Promise, but the native API expects a callback as the first argument. Same root cause shows up in a 2024 r/node thread on Passport.js: a strategy callback signature mismatched what Passport called it with, so the runtime tried to call cb(err, null) on something that was not a function. The fix is determined entirely by what x actually is at runtime, and there are five things it typically is.

Quick answer

The value you tried to call is not a function at that moment. Print typeof x and x on the line above the throw; the type tells you which of five causes (typo or wrong shape, wrong import / hoisting, overwritten binding, async race, lost this) applies. The 80%-of-the-time fix: log typeof x right before the call. The type points straight at the cause.

How to diagnose in 30 seconds

  1. Read the message. The token before is not a function is what you tried to call.
    TypeError: users.forEach is not a function
    TypeError: fetchJson is not a function
    TypeError: this.handleClick is not a function
    TypeError: (intermediate value).toLowerCase is not a function
  2. Log it on the line above:
    console.log("DEBUG", typeof users, users);
    users.forEach(u => ...);
  3. Match the typeof to a cause:
    • "object" → Cause 1 (wrong shape, typo, or method does not exist on this object).
    • "undefined" → Cause 2 (import wrong, hoisting / TDZ) or Cause 4 (race).
    • "string", "number" → Cause 3 (binding overwritten).
    • "function" but it still throws inside → Cause 5 (lost this inside the function).

Cause 1: typo or wrong shape (typeof x === "object")

The most common case, by a lot. Either you misspelled the method or the value is not what you think it is.

What it looks like:

// 1. Misspelled method (this is a real one from production)
document.getElementByID("user-form");
// TypeError: document.getElementByID is not a function

// 2. Right method, wrong shape
document.getElementsByClassName("btn").forEach(b => b.disabled = true);
// TypeError: document.getElementsByClassName(...).forEach is not a function

The first is a typo: it is getElementById, lowercase d. The second is shape: getElementsByClassName returns an HTMLCollection, which has no forEach. MDN’s HTMLCollection page lists exactly which methods you get (hint: not many).

Fix:

document.getElementById("user-form");

Array.from(document.getElementsByClassName("btn"))
  .forEach(b => b.disabled = true);

document.querySelectorAll(".btn").forEach(b => b.disabled = true);

Same family: calling .json() twice on a Response, calling .map on { results: [...] } when the array is one level deeper, calling .toLowerCase on a number.

Cause 2: wrong import name or hoisting / TDZ (typeof x === "undefined")

ESM and CommonJS disagree on default vs. named exports, and the bundler sometimes lets a wrong import compile silently. Hoisting bites you separately when you call a const/let binding before its initializer line.

What it looks like:

// Module exports: export default function fetchJson() { ... }
import { fetchJson } from "./api";   // named import, but it is the default
fetchJson();                         // TypeError: fetchJson is not a function

// Hoisting / TDZ flavor
sayHi();                             // ReferenceError or TypeError, depending on engine
const sayHi = () => console.log("hi");

Function declarations (function foo() {}) are hoisted including the body, so they can be called before their declaration. Function expressions assigned to const/let are not. Mixing the two styles in the same file is how this bug ships.

Fix:

import fetchJson from "./api";       // default import matches default export

// or, if the module is CommonJS:
// module.exports = { fetchJson };
import { fetchJson } from "./api";

const sayHi = () => console.log("hi");
sayHi();                             // call after declaration

If typeof fetchJson === "undefined", the import is wrong. If typeof fetchJson === "object", you imported the whole module bag. Either way, check the export line of the file you are importing from. Vite and Webpack both have quiet cases where an import resolves to undefined without a build error; the Vite troubleshooting guide calls out the patterns. TypeScript 5.7 with "verbatimModuleSyntax": true catches most of these at build time.

Cause 3: variable was overwritten (scope shadowing)

You defined a function, then later in the file shadowed it with something else.

What it looks like:

function formatDate(d) { return d.toISOString(); }
// ... 200 lines later, in a function or block scope
const formatDate = document.getElementById("date").value;
// ...
formatDate(new Date());              // TypeError: formatDate is not a function

The inner formatDate wins. Now it is a string. The pattern is especially nasty in minified production bundles, where the rename is invisible until you load source maps.

Fix:

function formatDate(d) { return d.toISOString(); }
const dateInput = document.getElementById("date").value;
formatDate(new Date());

Rename one. Turn on ESLint’s no-shadow rule so the linter stops you the next time. no-redeclare and no-func-assign also catch siblings of this bug.

Cause 4: async race / result undefined

You called the function before its module had finished initializing, or before a Promise resolved. This is the exact shape of the r/learnjavascript Geolocation thread linked above: the wrapper assumed the API returned a thenable, the API returned an integer (the watch ID), .catch on a number is not a function.

What it looks like:

// Bug: third-party SDK loaded async, used synchronously
<script src="https://sdk.example.com/v2.js" async></script>
<script>
  window.ExampleSDK.track("page_view");
  // TypeError: Cannot read properties of undefined (reading 'track')
  // or: window.ExampleSDK.track is not a function
</script>

Same shape in React with dynamic imports or useEffect timing.

function App() {
  const [util, setUtil] = useState();
  useEffect(() => { import("./util").then(m => setUtil(m)); }, []);
  return <button onClick={util.track}>Click</button>;
  // TypeError on first render: cannot read properties of undefined
}

Fix:

<script src="https://sdk.example.com/v2.js"></script>
<script>
  window.addEventListener("load", () => window.ExampleSDK.track("page_view"));
</script>

// React: render a fallback until the import resolves
return util ? <button onClick={util.track}>Click</button> : <Spinner />;

Cause 5: lost this binding

Classic on class methods passed as callbacks without binding. The Passport thread linked above is a variation: the strategy verify-callback was defined with the wrong arity, so when Passport called cb(err, user), the position that should have held the callback held something else.

What it looks like:

class Button {
  constructor() { this.label = "Save"; }
  handleClick() { console.log(this.label); }
}

const b = new Button();
document.querySelector("button").addEventListener("click", b.handleClick);
// click -> TypeError: Cannot read properties of undefined (reading 'label')

Fix — pick one:

class Button {
  label = "Save";
  handleClick = () => console.log(this.label);   // class field arrow, cleanest
}

class Button {
  constructor() {
    this.label = "Save";
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() { console.log(this.label); }
}

addEventListener("click", (e) => b.handleClick(e));   // wrap at the callsite

Class fields with arrow functions are stable in every Node version since 12 and every evergreen browser; that is the 2026-default form.

Decision tree: which cause is yours

Fix at the source (imports, binding, race) rather than at the call site (typeof === "function" guards). Guards hide the bug. TypeScript 5.7 with strict: true catches Causes 1, 2, and most of 5 at build time; if you are seeing them in a TS project, your strict mode is off.

FAQ

Why does forEach fail on an object?

Plain objects are not iterable in the array sense. Use Object.values(obj).forEach(...) or Object.entries(obj).forEach(([k, v]) => ...). JavaScript does not add forEach to every collection-like thing.

What is the difference between “is not a function” and “is not defined”?

“is not a function” fires when the variable exists but is not callable. “is not defined” fires when the name cannot be resolved in scope at all. One is a type mismatch, the other is a reference error.

How do I check if a value is callable before calling it?

typeof fn === "function". That covers every callable in JavaScript, including arrow functions, classes, generators, and async functions. fn instanceof Function works too but fails across realms (iframes, workers).

Does arrow function vs regular function matter here?

Yes, for this. Arrow functions inherit this from the enclosing scope; regular functions get their own this depending on how they are called. For class methods passed as callbacks, the arrow form (class fields) is the pattern that does not lose this.

Why does the same code work in Node but fail in the browser (or vice versa)?

Different module systems (CJS vs ESM) or different globals. global/globalThis vs window, require vs import. The symptom is the same: the thing you thought you imported is not where you thought, so calling it fails. Node 22 uses ESM by default with "type": "module" in package.json; CommonJS files need .cjs.

Can hoisting cause this error directly?

Yes. var declarations are hoisted with an initial value of undefined, so calling them before assignment throws is not a function. const/let hit the temporal dead zone and throw ReferenceError instead. Both bugs disappear when you put the call after the declaration.

Sources and further reading

Next steps

If the root cause is TypeScript not catching the error at build time, the TypeScript generics guide walks through the strictNullChecks and noImplicitAny flags worth turning on before anything else. For the async flavor of this bug (calling a function before it has loaded), the async/await vs promises guide goes deeper into the ordering patterns.

esc