Diagnosing Lost `this` Context in CoffeeScript: Fat Arrow vs Thin Arrow
A diagnostic guide for CoffeeScript's fat arrow (=>) vs thin arrow (->): recognize lost `this` context in callbacks, event handlers, and class methods; use a cause/effect table, ordered checks, and version‑aware fixes.
08 Jul 2025, 19:34 UTC

The Problem
CoffeeScript offers two function arrows: the thin arrow -> and the fat arrow =>. The thin arrow behaves like a plain JavaScript function — its this value is determined by how the function is called. The fat arrow captures the this value from the surrounding lexical scope at definition time. When a callback is passed to a library, runtime, or event system, the caller often sets this to something unexpected (the global object, undefined in strict mode, or a DOM element). The symptom is usually a TypeError: Cannot read property '...' of undefined or logic that silently operates on the wrong object.
Takeaway: If a function needs to retain the this of its defining class or module, use the fat arrow =>. Reserve the thin arrow -> for functions that either don’t use this or are intentionally designed to receive a caller‑supplied context.
Recognizable Conditions
- Callback
thisisundefinedor global: Passing a thin‑arrow method tosetTimeout,Promise.then, or array methods likeforEach. - Event handler
thisis the DOM element: Using->withaddEventListeneror jQuery’son; the library deliberately setsthisto the target element. superfails inside a callback:TypeError: Cannot read property 'call' of undefinedbecause thin arrows lack the[[HomeObject]]internal slot required forsuper.- Nested callback loses outer
this: A thin arrow defined inside a method does not inherit the method’sthis.
Diagnostic Table
| Context | Thin arrow -> | Fat arrow => |
|---|---|---|
| Class method definition | this = instance, super works | this = instance, super works |
| Callback passed to library | this = library‑chosen (often element) | this = lexical (class instance) |
| Nested callback inside method | this = lost/undefined | this = captured from outer method |
setTimeout/Promise callback | this = global/undefined | this = captured lexical |
Event handler (addEventListener) | this = target element | this = lexical (instance) |
Ordered Checks
- Locate the function definition — is it
->or=>? - Identify who calls it: your code, a library, or the runtime?
- Determine the expected
this: class instance, DOM element, or irrelevant? - If
thismust be the class instance and the caller overrides it, change->to=>. - If
superis used, the function must be=>or a class method (not a thin arrow).
Fixes
Capture instance this in a callback
# Before: thin arrow loses `this`
class Widget
constructor: ->
@value = 42
start: ->
setTimeout ->
console.log @value # @ is undefined/window
, 100
# After: fat arrow captures lexical `this`
class Widget
constructor: ->
@value = 42
start: ->
setTimeout =>
console.log @value # @ is the Widget instance
, 100
Event handler needing both element and instance
# Fat arrow captures instance; `e.currentTarget` gives the element
el.addEventListener 'click', (e) => @handleClick(e, @)
Preserve super in asynchronous callbacks
# Thin arrow breaks `super`
class Child extends Parent
save: ->
Promise.resolve().then ->
super() # TypeError: Cannot read property 'call' of undefined
# Fat arrow (or class method) preserves `super`
class Child extends Parent
save: ->
Promise.resolve().then =>
super() # works
Escalation Criteria
- Memory pressure from long‑lived closures: Fat arrows retain the lexical scope. If a callback lives longer than the instance (e.g., stored in a global cache), consider
Function.prototype.bindor a weak‑reference pattern to avoid leaks. - Duplicate
var _this = thisin compiled output: Nested fat arrows each create their own closure. Inspect the generated JavaScript (coffee -c -m file.coffee) and flatten nesting if the duplication is excessive. - TypeScript interop mismatches: Ensure
@types/coffeescriptmatches the targeted ECMAScript version (ES2015+ classes vs. ES5 prototypes) so thatsuperand arrow semantics align.
Version Differences and Verification
- CoffeeScript 1.x:
=>compiles toFunction.prototype.bind(this)polyfill.argumentsis available;new.targetis not. - CoffeeScript 2.x:
=>compiles to native ES6 arrow functions.argumentsis not available;new.targetisundefined(arrows cannot be constructors). - Class body fat arrows (CS2+): Become instance methods via
Object.defineProperty, not prototype methods. This affectsObject.getOwnPropertyNamesand serialization.
Practical verification steps
- Create a minimal class with a thin‑arrow callback passed to
setTimeout; observethisisundefined(strict) orwindow. Change to fat arrow; observethisis the instance. - Add an event listener with
el.addEventListener 'click', -> console.log(@); click shows the element. Change to=>; click shows the class instance. - Define a subclass method with
superinside a thin arrow passed toPromise.then; call it and catchTypeError. Movesuperto a fat arrow or class method; error disappears. - Compile with
coffee -c -m file.coffeeand inspect the generated JS: thin arrows become plain functions; fat arrows becomefunction() {}.bind(this)(CS1) or() => {}(CS2). - Run the test suite in both CoffeeScript 1.12.x and 2.7.x (or latest 2.x) to confirm version‑specific output differences for
=>andsuper.
Limitations
- Fat arrows cannot be used as constructors (
new Fn()throws). Do not use=>for factory functions intended to be called withnew. - Destructuring
thisin a fat arrow parameter list (=>{@prop} ->) does not work. Use=>and access@propinside the body. - Implicit return of the last expression in a fat‑arrow callback can leak values into promise chains or
Array.map. Add explicitreturnorundefinedwhen the return value is not intended.
Quick Reference
| Scenario | Arrow | Reason |
|---|---|---|
Class method that uses @ or super | => (or plain method syntax) | Lexical this + [[HomeObject]] |
Callback to library that sets its own this | => | Capture defining scope |
| Event handler needing both element and instance | => + e.currentTarget | Instance via closure, element via event |
Utility function with no this dependency | -> | No closure overhead |
| Function intended as a constructor | -> (or class) | Arrows cannot be constructed |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.