Debugging CoffeeScript Implicit Returns: When Functions Give Back More Than You Intended
CoffeeScript returns the last expression of every function and every branch. Diagnose leaked return values in jQuery handlers and loops, and fix them with explicit bare returns.
17 Nov 2025, 17:00 UTC

Every CoffeeScript function returns the value of its last expression, whether you asked for it or not. Most of the time this is convenient. The bugs appear when a function is supposed to return nothing — an event handler, a callback, a fire-and-forget utility — and instead leaks a value that changes downstream behavior. This guide helps you recognize the condition, trace it to the responsible expression, and fix it without breaking the branches you didn't look at.
The recognizable condition
You typically arrive here through one of three symptoms:
- A jQuery event handler that should let the browser proceed instead cancels the default action or stops propagation.
- A function returns an array you never constructed, and callers start failing on unexpected input.
- A value from deep inside a callback chain short-circuits a promise or changes a boolean check upstream.
The common cause: CoffeeScript inserts a return before the last expression of every function body — and before the last expression of every branch of an if/else. There is no such thing as a void function unless you make one explicitly.
Cause and diagnostic table
| Symptom | Likely cause | Where to look |
|---|---|---|
| Handler prevents default browser behavior | Last expression evaluates to false; jQuery treats a false return as preventDefault() plus stopPropagation() | Final line of the handler |
| Unexpected array returned | Function ends in a for loop or comprehension; loops compile to expressions that collect per-iteration values into an array | Trailing loop at the end of the function |
| Unexpected truthy/falsey value upstream | Implicit return from one branch of an if/else you didn't audit | Last expression of each branch, not just the function's final line |
| Side effects in a chain not firing | An early implicit return value alters promise or callback flow | Each callback's final expression |
Ordered checks
- Compile and read the JavaScript. Run
coffee -c file.coffeefrom the project root (requires thecoffeescriptpackage installed locally or globally) or paste the function into the browser-based compiler on coffeescript.org. Find the insertedreturnstatements. This shows you what actually leaves the function, removing all guesswork. Note that CoffeeScript 1.x and 2.x produce different output (2.x emits ES2015+), so confirm which compiler version your build uses before drawing conclusions about the emitted code. - Audit every branch, not just the last line. Implicit returns apply per branch. A function whose final line is safe can still leak a value from the last expression of an
ifblock. Check each exit path. - Check for trailing loops. If the function ends in
for x in list, the compiled output collects each iteration's value into an array and returns it. This is both a correctness bug and a wasted allocation on large lists. - Check handler contracts. For jQuery handlers specifically, ask: can the last expression ever be
false? A call like$(el).toggleClass('active')returns a jQuery object (truthy, harmless), but an expression likeisValid and save()can evaluate tofalse.
Fixes tied to findings
If the function should return nothing: end it with a bare return. This compiles to a plain return; and yields undefined.
# Before: returns the jQuery object, or false in some paths
$('a.nav').on 'click', (e) ->
$(this).toggleClass('active')
# After: explicitly returns undefined
$('a.nav').on 'click', (e) ->
$(this).toggleClass('active')
returnIf a trailing loop is the leak: keep the loop for its side effects but add a bare return after it, or restructure so the loop is not the final expression.
processAll = (items) ->
for item in items
save(item)
return # without this, returns an array of save() resultsIf only one branch leaks: add return at the end of that branch. Remember that fixing the function's final line does nothing for values returned from inside an if.
For public APIs: prefer explicit return someValue statements even where the implicit return would produce the same result. Intent then survives refactoring — a future edit that appends a line won't silently change the return value.
Verifying the fix
Two cheap checks confirm the diagnosis and the repair:
- Recompile with
coffee -cand confirm the emitted function now ends inreturn;(or returns only the intended value) on every branch. - Write a test asserting the function returns
undefined— for example, that a click handler's return value is notfalse— and confirm it fails before the fix and passes after. For the jQuery case, reproduce by clicking a link whose handler previously returnedfalse: navigation should have been blocked before the fix and proceed after.
Limitations and escalation criteria
These fixes are per-function patches. Escalate beyond them when:
- Implicit returns interact with a downstream Babel or bundler pipeline, where CoffeeScript 2's ES2015+ output is re-transpiled and return behavior becomes harder to reason about across layers.
- The module is consumed by typed JavaScript or TypeScript callers that declare a
voidreturn — leaked values will keep surfacing as type errors at the boundary. - You are touching many functions in the same file. At that point, converting the module to modern JavaScript is usually cheaper than auditing every branch of every function. CoffeeScript is in maintenance status, so new investment in the language should be weighed against migration cost.
Until then, the discipline is simple: treat the last expression of every function and every branch as part of your API surface, and say return when you mean nothing.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.