← Back to the library
FoundationsCore · 4 min

A method fails with "Cannot read properties of undefined" when used as a callback

“service.upload(file) works when called directly, but files.map(service.upload) or a click handler throws "Cannot read properties of undefined (reading 'api')", or this points at the button instead of the object.”

THE PRINCIPLE

When you pass a method as a value, pass a function that already knows its receiver: an arrow wrapper or a bound function.

FIRST MOVES

  1. Read the error's property name: it is the first this.x access inside the method, so the method ran without its object.
  2. Find where the method is passed as a value: map/forEach/then callbacks, addEventListener, setTimeout, destructuring const { save } = repo, or a framework prop.
  3. Fix at the call site with an arrow wrapper, or bind once and reuse the bound function (needed to remove an event listener later).
  4. If the method should always be detachable, define it as an arrow class field; it costs one function per instance.
  5. Enable @typescript-eslint/unbound-method (in recommended-type-checked) to catch new cases at lint time.

TOOLS: THEN → NOW

this.handle = this.handle.bind(this) in constructorsArrow wrappers at the call site, or arrow class fields for always-detached handlers
Discovering it at runtime@typescript-eslint/unbound-method with type information

PATTERN SNAPSHOT

class Uploader {
  constructor(private api: { put(file: File): Promise<void> }) {}
  upload(file: File) { return this.api.put(file); }
  handleDrop(event: DragEvent) {
    event.preventDefault();
    for (const file of event.dataTransfer?.files ?? []) void this.upload(file);
  }
}
const uploader = new Uploader(api);

files.map(uploader.upload);                 // TypeError: Cannot read properties of undefined (reading 'api')
zone.addEventListener("drop", uploader.handleDrop); // this is the zone element: this.upload is not a function

files.map((file) => uploader.upload(file)); // receiver kept at the call site
const onDrop = uploader.handleDrop.bind(uploader);
zone.addEventListener("drop", onDrop);      // same reference later for removeEventListener

CLOSE THE AI. EXPLAIN THIS.

Why does obj.method() work while const f = obj.method; f() throws, and why do class bodies make it a TypeError rather than a silent globalThis lookup?

SOURCES

Guide reviewed