TypeError: … is not a function возникает в выражении с круглыми скобками, когда JavaScript уже вычислил вызываемое значение, но оно оказалось не функцией. Например, в catalog.load() объект catalog может существовать, а catalog.load оказаться строкой, объектом или undefined.
Это важно отличать от ошибки доступа к свойству. Если сам catalog равен undefined, движок не доберётся до вызова и сообщит Cannot read properties of undefined (reading 'load'). Такой случай разобран в отдельной статье. Здесь получатель вызова существует; проверять надо значение перед () и то, откуда оно взялось.
Проверь значение метода перед вызовом
Быстрая диагностика в консоли
Начните с верхнего кадра стека, который относится к вашему коду. В строке 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. Если метод обязателен, молчаливый пропуск нарушит договорённость ещё сильнее.
Короткий порядок исправления
- Найдите в стеке конкретное выражение с
(). - Отделите получателя от вызываемого значения:
objectиobject.method. - Посмотрите фактические значения и
typeofнепосредственно перед сбоем. - Если callee не функция, проследите его до присваивания, аргумента, ответа API или импорта.
- Если callee — функция, перейдите во внутренний кадр стека и повторите разбор там.
- Исправьте место, где нарушилась форма данных, модуля или получателя; оставьте runtime-проверку только на внешней границе.
is not a function сообщает не о плохих круглых скобках, а о несовпадении между ожидаемой операцией и фактическим значением. Чем ближе проверка к месту, где это значение появилось, тем меньше защитных условий понадобится дальше.