7 Rules To Write Better CoffeeScript Code Without Pain

Guidelines to write readable CoffeeScript.

Intro picture

I often hear that CoffeeScript isn't that great, that not having parenthesis is not good for readability, and so on. I think that it mostly comes from the way people write CoffeeScript, rather than the language itself.

CoffeeScript allows you to write easy to read code in a fast way. It's close to pseudo-code, it strips everything unecessary and it's very good at highlighting wrong code designs. Specially, because it's based on indentations (by the way I recommend four spaces long indentation).

This article will give you hints on usual mistakes and will point you out some generic programing pitfalls, like nesting too much code.

1. There is no absolute rule

This is actually the only absolute rule. Which proves my point :) Ruleception! It's not because you can read "no parenthesis in CoffeeScript" that you mustn't use them when necessary. In the same way, it isn't because it's possible that you should do it at all cost. Don't forget they are guidelines, not The Truth.

2. Keep it small

In introduction I said CoffeeScript is based on indentation. Thus, having such a limit is very good at showing where you have too much nested code, thus helping you improving your code. This is a completely generic programming advice though!

Define a charaters per line limit

We use a 80 characters limit at Cozy and I'm very happy with it. I'd like to add that I've seen an other common recommendations at 120 characters, but 80 has proven itself to be just perfect again and again for me. People saying it's too little probably write too nested code ;-)

A good idea to rember that limit is to set a vertical ruler in your favorite IDE (I use SublimeText but almost all of them have the feature).

Write short functions

The end of big functions are hard to track because CoffeeScript is indentation-based, and big functions tend to be complex with many conditional structures. Eventually, asynchronous calls make it even worse. The deep issue is always nested code, big functions are just a symptom. So here is my advice: use the symptom to cure the disease!

3. Avoid parenthesis

One of the thing I love with CoffeeScript is that most parenthesis are not mandatory. In fact, most parenthesis can be removed.

Let's answer the question: when should you use parenthesis?

  • In conditional statements (if, loops), always use parenthesis to prevent wrong interpretation. Always putting parenthesis avoids the risk of any possible buggy statement.

{% highlight coffeescript %}

valid

if 'something' is myObj.myFunc 'some-parameter'

invalid

if myObj.myFunc 'some-parameter' is 'something'

because it's an equivalent of

if myObj.myFunc('some-parameter' is 'something') {% endhighlight %}

  • When you chain function calls

{% highlight coffeescript %}

valid, still readable

myVar = myFunc1 myObj.get 'my-property'

valid, less readable

myVar = $.myFunc1 anObj.myFunc2 myObj.get 'my-property'

valid, unreadable

myVar = $.myFunc1 anObj.myFunc2 $.myFunc3 _.myFunc4 myObj.get 'my-property'

valid, and readable

myVar = $.myFunc myObj.get('my-property')

valid, and readable

myVar = $.myFunc1 myObj.myFunc2(myObj.get('my-property'))

valid, and unreadable

myVar = $.myFunc1 myObj.myFunc2($.myFunc3(_.myFunc4(myObj.get('my-property')))) {% endhighlight %}

You can try more examples with parameters for the various functions, parenthesis versions are almost easier to read.

But where do you set the limit? How many functions in the chain before using parenthesis? Chose one and stick to it! I find relevant to set the limit to 1 for two reasons. First, if you have to many chain calls, something might be wrong. Second, it might be easier to understand for people that are not used to read CoffeeScript.

4. Explicit returns

The last line of every piece of code is turned into a return call. Please, don't do implicit return, most of the time it makes things confusing.

{% highlight coffeescript %}

valid, but not readable

myFunc = -> myString = substring 'example', 1 something = myString + anotherString

something

valid, and readable

myFunc = -> myString = substring 'example', 1 something = myString + anotherString

return something

{% endhighlight %}

As no rule is absolute, there is some case where implicit return is handy:

{% highlight coffeescript %}

valid, readable, and concise

myMappedCollection = myCollection.map (object) -> object.property {% endhighlight %}

I won't blame anyone willing to always use an explicit return, though.

Explicit returns also allow you to track the end of function more easily if you haven't managed to make it small enough. It's all good!

5. Control your flow

The advice I'm going to give are highly controversial. We do not agree on the right way of doing things within the team. So I'm going to expose you the problem and suggest some solutions.

I've been talking how bad nested code is, especially in CoffeeScript (it's even more important because know you have a 80 characters per line limit!). In JavaScript, we tend to write a lot of asynchronous stuff which makes it even worse. Among the use cases, XHR (Ajax requests) is an obvious one. If you do NodeJS you will find plethora of examples. Putting that straight, asynchronous calls are generate a lot of asynchronous call. They will turn your code into a mess if you're not careful. Flow control in JavaScript deserves an article on its own so let me just introduce you to the issue.

You want to retrieve multiple informations from the server to say, pre-fill a form, but that could be anything really.

{% highlight coffeescript %} getUserInformation (err, user) -> getCountryInformation (err, country) -> getOrders (err, orders) -> getRelatedProducts (err, relatedProducts) -> # do something with all those data {% endhighlight %}

Good, now let's handle the error case

{% highlight coffeescript %} $.get '/user/:id', (err, user) -> if err? # do something with err else $.get '/country/:id', (err, country) -> if err? # do something with err else $.get '/orders/:userId', (err, orders) -> if err? # do something with err else $.get '/relatedProducts/:userId', (err, products) -> if err? # do something with err else # do something with all those data {% endhighlight %}

You could argue that we can handle the error case with one liner, but I just to make a point: nesting functions with conditional structures lead to the callback hell.

To solve this, you can use subfunctions, but that doesn't scale well because you end not knowing if you are in your main function or in a subfunction, and having to navigate between your main function body and the subfunctions to know what is going on.

You can also use Promises, but people disagree on where Promises should be used and where they shouldn't.

What I want to suggest you is using Async, a popular library. Async has the big drawback of being very big but if you find yourself falling into this again and again in your code, it might worth it.

{% highlight coffeescript %}

same code, with async

async = require 'async'

all requests will be run in parallel

async.parallel [ (cb) -> $.get '/user/:id', cb (cb) -> $.get '/country/:id', cb (cb) -> $.get '/orders/:userId', cb (cb) -> $.get '/relatedProducts/:userId', cb ], (err, results) -> if err? # deal with the error else [user, country, orders, relatedProducts] = results # do something with the data {% endhighlight %}

6. It's small but it matters

The last rule is actually a bunch of them. There are small details that are nice improvements to the way you write CoffeeScript.

  • Write operators in english, e.g. use is instead of == and and for &&.
  • Don't use {} to define objects unless it's mandatory.
  • To assign value to variable only if it's undefined use ?=.
  • If your array or object is defined on several lines don't use commas.
  • Do you see something else? Let us know!

7. Use a linter

This true whatever language you use, but lint your code. An automatic checking of all your code style rules you define is very good to have. We use CoffeeLint, especially to track characters per line limit and indentation issues, but there are many more options that could fit your use.

Don't forget that the big deal is coherence in your code, once you have rules, write them down somewhere for your whole team and stick to them.

So What did you think of this article? What guidelines do you suggest? Let's share them on the forum!