CoffeeScript's Fat Arrow: Lexical `this` Without the Boilerplate
CoffeeScript's fat arrow (=>) captures the surrounding `this` at compile time, eliminating the classic `var self = this` boilerplate. This post shows how it works, what the compiler emits for ES5 vs ES6 targets, and the trade-offs you face when debugging or migrating.
24 Aug 2025, 10:58 UTC

The callback context problem
JavaScript developers have long struggled with the this keyword inside callbacks. Pass a method as an event handler or a setTimeout callback, and this suddenly points to the global object (or undefined in strict mode) instead of the instance you expected. The classic workaround — var self = this; — clutters every method that uses asynchronous APIs.
CoffeeScript introduced the fat arrow (=>) to solve this at the language level. When you write a function with =>, the compiler emits code that captures the surrounding this and uses it inside the function body, giving you lexical scoping for this years before ES6 arrow functions arrived.
Thin arrow vs. fat arrow
CoffeeScript provides two arrow styles:
->(thin arrow) compiles to a plain JavaScript function expression.thisis determined by how the function is called.=>(fat arrow) compiles to a function that closes over the currentthisvalue, preserving it regardless of call site.
The distinction is deliberate: use -> for functions that should receive their own this (e.g., constructors, prototype methods), and => for callbacks that need the enclosing context.
What the compiler actually emits
Consider this CoffeeScript class:
class Counter
constructor: ->
@count = 0
increment: =>
@count++
console.log @count
start: ->
setTimeout @increment, 1000
Compiled with coffee -c targeting ES5 (the default for older CoffeeScript versions), you get something like:
var Counter = (function() {
function Counter() {
this.count = 0;
}
Counter.prototype.increment = function() {
var _this = this;
return function() {
_this.count++;
return console.log(_this.count);
};
};
Counter.prototype.start = function() {
return setTimeout(this.increment, 1000);
};
return Counter;
})();
Notice the increment method: the outer function captures this into _this and returns an inner function that uses _this. The start method uses a thin arrow, so this.increment is passed as a plain function; when setTimeout calls it, this inside increment would be lost if increment hadn't already bound its context via the fat arrow.
If you compile with --target es6 (available in CoffeeScript 2+), the fat arrow becomes a native ES6 arrow function, and the _this wrapper disappears:
class Counter {
constructor() {
this.count = 0;
}
increment = () => {
this.count++;
console.log(this.count);
}
start() {
setTimeout(this.increment, 1000);
}
}
This difference matters when debugging: the ES5 output adds an extra stack frame, while the ES6 output is a single arrow function.
A worked example: event handlers in a UI component
Imagine a simple tooltip widget that shows a message on hover:
class Tooltip
constructor: (@element, @message) ->
@element.addEventListener 'mouseenter', @show
@element.addEventListener 'mouseleave', @hide
show: =>
@tooltip = document.createElement 'div'
@tooltip.textContent = @message
document.body.appendChild @tooltip
hide: =>
@tooltip?.remove()
@tooltip = null
Both show and hide use fat arrows. When the browser calls them as event handlers, this still refers to the Tooltip instance. Without the fat arrow, this would be the @element (the event target), and @tooltip would be undefined.
You can verify the behavior by running the compiled JavaScript in a browser console and inspecting this inside each handler.
Trade-offs and limitations
- Debugging opacity: The ES5 wrapper (
var _this = this) adds an anonymous function layer. Stack traces show an extra frame, and setting breakpoints inside the fat arrow may land in the wrapper rather than your source line (source maps mitigate this). - Target version divergence: Code that works identically in source can produce drastically different output depending on
--target. If you ship ES5 bundles, the_thispattern increases bundle size slightly. - Migration friction: Teams moving to modern JavaScript must replace
=>with ES6 arrows (() => {}) and->withfunction() {}or() => {}depending onthisintent. The mental model shifts from "fat arrow binds this" to "arrow functions don't have their own this". - No partial application: CoffeeScript's fat arrow always binds the current
this. You cannot create a bound function with a differentthiswithout reverting toFunction.prototype.bindor a thin arrow plus manual binding.
Practical verification steps
- Create a file
demo.coffeewith theCounterclass above. - Run
coffee -c demo.coffee(ES5) andcoffee -c --target es6 demo.coffee(ES6). - Open the generated
demo.jsfiles and compare theincrementmethod. - Execute each in Node.js (
node demo.js) and confirm the logged count increments correctly.
If the count increments once per second, the lexical binding works. If it throws Cannot read property 'count' of undefined, the fat arrow was not applied or the target compilation dropped the binding.
Closing thought
CoffeeScript's fat arrow was a pragmatic solution to a real JavaScript pain point. It gave developers lexical this without waiting for browser support. Today, with ES6 arrows ubiquitous, the same pattern is native. If you maintain a CoffeeScript codebase, understand the compilation target: ES5 output uses the _this pattern, ES6 output uses real arrows. When planning a migration, map each => to an ES6 arrow and each -> to a regular function (or arrow if it never uses this). That preserves behavior while moving to standard syntax.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.