TypeError: … is not a function в JavaScript — как найти не-функцию

JavaScript Автор: Среда и версия: ECMAScript 2026, ES modules и Node.js CommonJS
содержание

TypeError: … is not a function возникает в выражении с круглыми скобками, когда JavaScript уже вычислил вызываемое значение, но оно оказалось не функцией. Например, в catalog.load() объект catalog может существовать, а catalog.load оказаться строкой, объектом или undefined.

Это важно отличать от ошибки доступа к свойству. Если сам catalog равен undefined, движок не доберётся до вызова и сообщит Cannot read properties of undefined (reading 'load'). Такой случай разобран в отдельной статье. Здесь получатель вызова существует; проверять надо значение перед () и то, откуда оно взялось.

JavaScript / 01

Проверь значение метода перед вызовом

Проверь значение метода перед вызовом01 Найди вызов formatter.format(value) Начни с верхнего кадра стека. 02 Выдели вызываемое typeof formatter.format Проверь фактический formatter и его поле format. 03 Выбери действие function → вызвать другой тип → чинить источник При вызове метода сохрани formatter как this.01Найди вызовformatter.format(value)Начни с верхнего кадра стека.02Выдели вызываемоеtypeof formatter.formatПроверь фактический formatter и его поле format.03Выбери действиеfunction → вызватьдругой тип → чинить источникПри вызове метода сохрани formatter как this.
Проверь получателя и значение перед скобками вызова: formatter.format должен быть функцией. Если это другой тип, ищи причину у источника данных.

Быстрая диагностика в консоли

Начните с верхнего кадра стека, который относится к вашему коду. В строке apiClient.send(order) вызываемое выражение — apiClient.send, а получатель — apiClient. Сохраните оба значения, но пока не вызывайте функцию отдельно:

const receiver = apiClient
const callee = receiver.send

console.log({
  receiver,
  receiverType: typeof receiver,
  callee,
  calleeType: typeof callee,
})

Если calleeType равен "string", "object" или "undefined", причина подтверждена. Дальше найдите последнее присваивание send, место создания apiClient либо его импорт. Для прямого вызова transform(data) достаточно проверить сам идентификатор:

console.log({ transform, type: typeof transform })

Не делайте вывод только по имени переменной или подсказке редактора: ошибку вызвало значение во время конкретного запуска. Поставьте точку останова перед вызовом и двигайтесь назад по присваиваниям, параметрам функции и результатам импорта.

typeof value === "function" тоже не доказывает, что весь вызов завершится успешно. Классы дают тип "function", но требуют new, а обычная функция может выбросить TypeError уже внутри себя. Если callee — функция, откройте следующий кадр стека и разберите внутренний вызов:

function submit(order) {
  return order.save()
}

submit({ save: false })
// TypeError: order.save is not a function

Здесь submit исправно вызвана. Нарушен контракт её аргумента: order.save содержит логическое значение вместо функции.

Имя функции затенили или перезаписали

Параметр, локальная переменная или свойство может получить то же имя, что и функция. В этом примере параметр sendOrder скрывает внешнюю функцию:

const sendOrder = (order) => fetch("/api/orders", {
  method: "POST",
  body: JSON.stringify(order),
})

function checkout(order, sendOrder) {
  return sendOrder(order)
}

checkout({ id: 17 }, { status: "ready" })
// TypeError: sendOrder is not a function

Исправление — не проверка typeof перед каждой отправкой. Передайте функцию и назовите роли так, чтобы объект состояния нельзя было принять за операцию:

function checkout(order, send) {
  return send(order)
}

checkout({ id: 17 }, sendOrder)

Для функции, которую не планируют переназначать, используйте const. Если имя приходит параметром, проверьте вызывающий код: именно он нарушил договорённость и передал объект вместо функции.

Метод вызвали у значения другого типа

Сообщение часто называет существующий метод, но получатель имеет другую форму. Пусть клиент API возвращает объект с массивом items, а вызывающий код ожидает сам массив:

async function loadProducts() {
  const response = await fetch("/api/products")
  return response.json()
}

const products = await loadProducts()
const names = products.map((product) => product.name)
// TypeError: products.map is not a function

map не пропал: переменная products содержит объект ответа. Если его контракт выглядит как { items: [...] }, обращайтесь к полю явно:

const result = await loadProducts()
const names = result.items.map((product) => product.name)

Если остальное приложение должно получать массив, распакуйте ответ один раз внутри loadProducts() и сохраните единый контракт. Не добавляйте typeof products.map === "function" во все места использования: такая проверка лишь превратит ошибочные данные в пропущенную работу.

Тот же принцип действует для встроенных методов: trim() ожидают у строки, map() — у массива, get() — у Map. Сначала проверьте фактическое значение получателя, затем сверяйте способ доступа с формой данных.

Default, named и namespace import — разные формы

Импорт пространства имён возвращает объект модуля. Даже если в модуле есть default, сам namespace-объект не становится этой функцией:

// format-price.js
export default function formatPrice(value) {
  return `${value} ₽`
}
import * as formatPrice from "./format-price.js"

formatPrice(1200)
// TypeError: formatPrice is not a function

Для default export нужен default import:

import formatPrice from "./format-price.js"

formatPrice(1200)

У именованного экспорта форма другая:

// format-price.js
export function formatPrice(value) {
  return `${value} ₽`
}

// checkout.js
import { formatPrice } from "./format-price.js"

При статическом ES-импорте отсутствующий export обычно обнаруживается ещё при загрузке модуля как SyntaxError, до выполнения вызова. is not a function чаще появляется, когда объект модуля пришёл через namespace import, динамический импорт, CommonJS, мок или прослойку сборщика и код вызвал весь объект вместо нужного свойства.

В CommonJS значение require() равно module.exports. Поэтому эти два модуля возвращают разные формы:

// formatter.cjs
exports.formatPrice = (value) => `${value} ₽`

// checkout.cjs
const formatter = require("./formatter.cjs")

formatter.formatPrice(1200)

Вызов formatter(1200) здесь упадёт: require() вернул объект с методом. Исправьте импорт или экспорт так, чтобы обе стороны соглашались о форме модуля; не прикрывайте несовпадение запасной цепочкой mod.default || mod.

Потерянный this проверяйте только по месту вызова

Обычный метод получает this из выражения вызова. Если перенести функцию на другой объект, получатель изменится:

const cart = {
  total() {
    return 1200
  },
  checkout() {
    return this.total()
  },
}

const job = {
  checkout: cart.checkout,
  total: 1200,
}

job.checkout()
// TypeError: this.total is not a function

Здесь checkout — функция, но при job.checkout() значением this становится job, а job.total — число. Если обработчик должен всегда работать с cart, сохраните исходный вызов через обёртку или привязку:

const jobWithWrapper = {
  checkout: () => cart.checkout(),
}

const jobWithBinding = {
  checkout: cart.checkout.bind(cart),
}

Не списывайте любой is not a function на «потерю контекста». В ES-модуле простой отрыв const checkout = cart.checkout; checkout() даст this === undefined; попытка прочитать this.total относится уже к ошибке Cannot read properties of undefined. Контекст — подходящая версия только тогда, когда стек ведёт внутрь метода, сообщение называет вызов через this, а фактический получатель отличается от ожидаемого.

Почему optional chaining и повсеместный typeof не исправляют контракт

Защитная проверка уместна на настоящей границе: например, приложение загружает сторонний плагин, а его публичный договор требует метод run. Тогда лучше остановиться сразу с точным сообщением:

if (typeof plugin.run !== "function") {
  throw new TypeError("plugin.run must be a function")
}

plugin.run(context)

Внутри собственного кода повторять такую проверку перед каждым вызовом невыгодно. Она разносит знание о контракте по проекту и часто скрывает место, где значение испортилось. Исправляйте один источник: распакуйте ответ API в адаптере, согласуйте export с import, переименуйте затеняющий параметр или сохраните правильный this.

Optional chaining решает другую задачу. plugin?.run() пропустит вызов только при null или undefined в plugin, но не превратит run: "later" в функцию. Запись plugin.run?.() пропустит отсутствующее свойство, однако существующее не-функциональное значение всё равно приведёт к TypeError. Если метод обязателен, молчаливый пропуск нарушит договорённость ещё сильнее.

Короткий порядок исправления

  1. Найдите в стеке конкретное выражение с ().
  2. Отделите получателя от вызываемого значения: object и object.method.
  3. Посмотрите фактические значения и typeof непосредственно перед сбоем.
  4. Если callee не функция, проследите его до присваивания, аргумента, ответа API или импорта.
  5. Если callee — функция, перейдите во внутренний кадр стека и повторите разбор там.
  6. Исправьте место, где нарушилась форма данных, модуля или получателя; оставьте runtime-проверку только на внешней границе.

is not a function сообщает не о плохих круглых скобках, а о несовпадении между ожидаемой операцией и фактическим значением. Чем ближе проверка к месту, где это значение появилось, тем меньше защитных условий понадобится дальше.

Источники