A Case Against Using CoffeeScript

By Ryan Florence, published 2011-12-10

Update: I actually love CoffeeScript now that I've been writing it for a year. I hope to write something about my conversion soon. You might want to see these articles: Rewriting Some JavaScript to CoffeeScript

Debugging CoffeeScript with Source Maps So anyway, you may proceed...

I recently started working at Instructure, an LMS provider that’s, for the most part, a ruby on rails shop. The team had started using CoffeeScript before I showed up, but gave the decision to me to keep it, or kick it to the curb. I think it says a lot about our company that we can use bleeding edge technology in our flagship product, and I’d hate to make any decisions that would hamper that culture. So, I write CoffeeScript at work.

Additionally, I’m writing an experiemental CoffeeScript branch of SnackJS. Once that’s done, I think I’ll have as much experience with using CoffeeScript as anybody, and probably more than most. I’ll have written a library by myself, and worked in a large application with other engineers in a public facing product.

But for now I’m still on the fence. However, this post is going to sound like I hate CoffeeScript. After a couple months of reviewing and writing CoffeeScript I’ve got a few beefs I’d like to get off my chest. Since there’s very little educated criticisms of the language out there, I’m happy to fill the gap :)

Debugging is an Issue

My code is more readable for me than yours. That’s just how it is. While the JavaScript CoffeeScript compiles looks decent, it’s still not mine. This is a big deal. If I were making a list of things I really really love, my CoffeeScript workflow wouldn’t make it, check it out:

The CoffeeScript Debugging Workflow

First I have to find where the problem is in the JavaScript since it doesn’t compile line-for-line to my CoffeeScript. Then I have to comprehend the JavaScript because it’s not my code. After I comprehend the JavaScript I can work on discovering the problem. Once I discover the problem I have to find the code in the CoffeeScript to fix it. Fix it in the CoffeeScript. Compile (usually automated, but still a step) Problem fixed, move on, otherwise… Wonder if the compile process was successful. Go back to the JavaScript to see if it’s what I expect, start over.

In an attempt to avoid all of this shuffling between languages, I’m back to putting in a lot of console.log statements in my code instead of using the amazing tools in webkit’s inspector :( I do it because I can just watch the log to observe state, and keep my brain in CoffeeScript-land, in my code. A far less productive development experience.

Compare that workflow with my JavaScript workflow:

The JavaScript Debugging Workflow

Start discovering the problem in the code I wrote. Fix it in the JavaScript Problem fixed, move on, otherwise start over.

I make far greater use of the debugging tools in pure JavaScript because my brain can stay in the exact context it’s been in the whole time, in the code I wrote–not to mention I can copy/paste my code into the console to see if it works better right then.

It’s a big deal. If you’re just fading elements in and out, you might not have to debug. But if its anything non-trivial you’re going to spend more of your time in the JavaScript, not the CoffeeScript–so what’s the point?

Verbally Readable !== Quicker Comprehension

Verbally readable is not exactly the same as quicker comprehension.

See what I did there? The main argument for using CoffeeScript is that it’s more readable and should therefore be easier to write, debug and maintain. However, reading something and comprehending something are very different.

We Process Images and Symbols Faster than Words

There’s this myth out there (started by perl? rubyists?) that code that is verbally readable is more comprehensible than code that isn’t as comfortable to verbalize. Psychologists have found that words are comprehended with our short-term memory which is generally limited to about 7 bits of information while images go straight to long-term, which makes images much faster to process.

Is it easier to explain what a circle is or to just show it?

We Feel Images

Additionally, images and symbols elicit a feeling that leads to immediate understanding. If you told your mom “Ryan Florence has a really cute baby boy” she’d probably ask “who is Ryan Florence?”. But if instead you just showed her this picture she’d have an emotional response and immediately understand that my kid is really cute.

This All Applies to “Ugly” Syntax

We can recognize and comprehend a symbol in programming syntax much faster than we can comprehend a key word.

Compare:

if (five && six && seven) doStuff();

With:

doSomething() if five and six and seven

My eyes jump over the && characters because they are immediately comprehended, along with the relationship between the variables on either side. In my mind I hear if five six seven do stuff . Even though I don’t say “and”, I feel the relationship. On the flip side, I actually have to read all the and characters in the CoffeeScript.

You feel pictures and symbols, you feel the relationship.

one !== two isSet || isDefault one isnt two isSet or isDefault

Can you feel it?

One-liners are Horrible, Tempting

CoffeeScript encourages writing code in sentences instead of logical statements.

eat food for food in foods

That looks great on the demo page, but eventually you end up with something more like this:

wash plate, brush, sink for key, plate of dishes when plate.dirty if meal.status is 'done'

That is verbally readable code, but it’s not very comprehensible:

What is plate ? The left side of the line (the side you scan while reviewing and debugging code) depends on stuff in the middle. You have to see plate then find it later in the line to know what it is, then come back to see what you’re doing with it. Why is the plate still dirty in the app?! There are three things that could screw up that single line of code dishes is empty, 8 words in you know you’re dealing with dishes. The plate isn’t dirty, 10 words in. The meal.status isn’t done, at the very end of the line

So all the things that determine whether or not the plate gets washed are deep inside the line. Though easy to read out loud, very hard to comprehend while scanning. Verbally readable code states the logic backwards. To comprehend it, you have to understand it from right to left, which takes more time. I’d probably write the wash plate line like this, assuming its inside of a function:

if meal.status isnt 'done' return for key, plate of dishes if plate.dirty wash plate, brush, sink

Totally clear when a block of code will run, and much easier to scan. Also, not substantially different than some fast-and-loose JavaScript:

if (meal.status !== 'done') return for (var plate in dishes) if (dishes[plate].dirty) wash(dishes[plate], brush, sink);

Though, I’d write it like this:

if (meal.status !== 'done') { return; } for (var plate in dishes) { plate = dishes[plate]; if (plate.dirty) { wash(plate, brush, sink); } }

The one-liners are very tempting as the author: you’re head is already wrapped around the problem so it’s easier to stay in context, giving you the false idea that it’ll make more sense to everybody else. Don’t believe me? Check out this one-liner I pulled out of the first .coffee file I randomly opened from our application:

scores = (student["assignment_#{@assignment.id}"].score for own idx, student of @gradebook.students when student["assignment_#{@assignment.id}"]?.score?)

That’s 160 columns of “readable code” that turns into this for debugging ಠ_ಠ:

var idx, scores, student; var __hasProp = Object.prototype.hasOwnProperty; scores = (function() { var _ref, _ref2, _results; _ref = this.gradebook.students; _results = []; for (idx in _ref) { if (!__hasProp.call(_ref, idx)) continue; student = _ref[idx]; if (((_ref2 = student["assignment_" + this.assignment.id]) != null ? _ref2.score : void 0) != null) { _results.push(student["assignment_" + this.assignment.id].score); } } return _results; }).call(this);

CoffeeScript has its own “bad parts”

It really is a beautiful language to look at–just not all the time.

This is really weird:

getUser = (id) -> url = "users/#{id}" dfd = $.ajax url: url format: 'json' method: 'get' url: url promise: dfd.promise()

It’s hard to recognize instantly that I’m actually calling $.ajax , not just assigning it. If you don’t know CoffeeScript, I’m sure the last two lines are totally baffling. Maybe I just need more time, but I feel like a parser sometimes trying to decide what’s happening. I actually like the parens to let me know what’s going on without having to look at the line-breaks and whitespace.

To be more clear about what I’m doing I’d like to do this:

getUser = (id) -> url = "users/#{id}" dfd = $.ajax url: url format: 'json' method: 'post' return url: url promise: dfd.promise()

But I can’t, that’s a parse error, extremely similar to one of the “bad parts” CoffeeScript aims to cover: automatic semi-colon insertion (ASI).

return { foo: 'bar', baz: 'qux' } // not returned due to ASI

Allman-style braces are death anyway, but still, interesting similarity there. Why can’t I return an object the same way I pass it as an argument?

Fat-arrow is an Anti-Pattern

The fat-arrow seems cool. Code that would look like this using jQuery:

var widget = { attach: function () { this.el.bind('click', $.proxy(function (event) { doStuffWithThis(); }, this)); } };

Becomes this:

widget = attach: -> el.bind 'click', (event) => doStuffWithThis()

Passing around a bunch of anonymous callbacks is an anti-pattern that leads to really terrible whitespace problems. Now with the fat-arrow, people are encouraged to fat-arrow their way to oblivion.

Even in CoffeeScript, you should still do this:

widget = attach: -> el.bind 'click', $.proxy this, 'handler' handler: (event) -> doStuffWithThis()

I could talk a lot more about why I don’t like the fat-arrow, but this article is already losing your attention at this point. Another time.

Significant White-space + Spaghetti === Death

Significant white-space is pretty cool, and there are good arguments for and against it. I’m not going to go too far down this path. But the truth of the matter is that most JavaScript code in user-land is garbage. People will write CoffeeScript as horribly as they wrote JavaScript: 30 line anonymous callbacks nested several levels, endless jQuery chains with event handlers, sloppy conditional control ( if else if if elseif else if unless else , maybe even worse to get automatic returns), you name it, you’ll see it.

Significant-whitespace is pretty rough in that kind of code. I’d bet that sorting out horrible CoffeeScript is going to be harder than sorting out horrible JavaScript.

Significant White-space Means CoffeeScript Will Always Be Compiled

It makes no sense for a web scripting language to have significant white-space; you can’t compress it. Therefore it will never really be supported natively, and will always be a compile-to-JS language, and will therefore always have a terrible debugging experience.

CoffeeScript is Beautiful; Don’t Use It

I think CoffeeScript is a ton of fun to write in the moment. I also think it’s going to be a maintenance nightmare because it is more difficult to comprehend and debug–and that, after all, is what we do most of the day.