Skip to content

Instantly share code, notes, and snippets.

@meagar
Last active December 29, 2015 11:19
Show Gist options
  • Save meagar/7663245 to your computer and use it in GitHub Desktop.
Save meagar/7663245 to your computer and use it in GitHub Desktop.
CoffeeScript gotcha - implicit objects on multiple lines

Wrapping binary operands to two lines

A silly one I encountered trying to put the operands to binary + on two lines, mostly my fault.

Gotcha

I had a simple bit of math involving some long operands. Here's a contrived example:

canvas.width -= parseInt(@$dialog.find('some-element').css('padding-left'), 10) + parseInt(@$dialog.find('.some-element').css('padding-right'), 10)

This line is a bit of a monster, so I made my first attempt at wrapping it to two lines:

canvas.width -= parseInt(@$dialog.find('some-element').css('padding-left'), 10)
             + parseInt(@$dialog.find('.some-element').css('padding-right'), 10)

This failed to compile, so I tried bumping the + back to the start of the line:

canvas.width -= parseInt(@$dialog.find('some-element').css('padding-left'), 10)
+ parseInt(@$dialog.find('.some-element').css('padding-right'), 10)

This compiled fine! I actually (here's where the my-fault part comes in) committed and pushed this code, after verifying that it compiled, but before seeing if it did what I thought it did.

Of course, it didn't. It actually compiles to two statements, an assignment and a unary plus:

canvas.width -= parseInt(@$dialog.find('some-element').css('padding-left'), 10);

+parseInt(@$dialog.find('.some-element').css('padding-right'), 10);

This is actually completely correct and desirable behaviour. CoffeeScript can't know whether you wanted the binary or the unary form of + (or -, if I'd used that) so it has to assume I wanted the unary one. Had I been using an operator that doesn't have a unary version, like *, both versions would have given me a compilation error.

Solution

Leave the operator in a binary operation on the first line:

canvas.width -= parseInt(@$dialog.find('some-element').css('padding-left'), 10) +
  parseInt(@$dialog.find('.some-element').css('padding-right'), 10)

Of course, the real solution is to chop it up into shorter statements:

left = parseInt(@$dialog.find('some-element').css('padding-left'), 10) 
right = parseInt(@$dialog.find('.some-element').css('padding-right'), 10)

canvas.width -= left + right

Indenting chained method bodies

tl;dr Real world example

Kind of a long one, so here's a realistic example of where this could bite you badly:

$('.dialog-button')
.hover -> $(this).css('background', '#foo')
.click (e) ->
  e.preventDefault()
  $('div#dialog').fadeIn('slow')

Our intent is to bind to the click and hover handlers of a button which shows a dialog. However, our click handler is being bound inside the hover callback, chained to $(this).css('background', '#foo'):

$('.dialog-button').hover(function() {
  return $(this).css('background', '#foo').click(function(e) {
    e.preventDefault();
    return $('div#dialog').fadeIn('slow');
  });
});

The worst kind of bug. At first, everything works great. In hover, our click callback is bound to the right element, as this and .dialog-button are the same element. When we click our dialog appears as expected. We move on to impelement something else, assured that CoffeeScript is doing exactly what we think it's doing.

However, each time we hover over the button, a new click handler is bound. Over time, our click handlers are piling up, and suddenly clicking on the button invokes dozens of fadeIn animations on the same element. This is the kind of ugly bug you'd likely only find by examining the compiled JavaScript in detail.


Details

An annoying (seeming) inconsistency in CoffeeScript's indentation

We have a method returning a promise (or other chainable object), and we want to invoke a few additional methods on it such as done or fail:

$.get('/users')
  .done ->
    #
    # <several lines of code>
    #
  .fail ->
    showError()

Our fail callback is pretty short, so we might be tempted to put it on one line:

$.get('/users')
  .done ->
    #
    # <several lines of code>
    #
  .fail -> showError()

This works fine, but it has opened to door for a few weird gotchas.

Gotcha

Suppose our done turns out to be shorter than we thought. We might refactor both callbacks to a single line:

$.get('/users')
  .done -> showSuccess()
  .fail -> showError()

This looks sane at first glance, but it's actually invoking fail on the return value of showSuccess():

$.get('/users').done(function() {
  return showSuccess().fail(function() {
    return showError();
  });
});

If showSuccess happens to return a promise which also responds to fail, this entire block of code will work just fine until we discover that our error handling code isn't handling our errors, likely leading to a really ugly user experience.

Another way it might fail silently:

If we'd decided to reorganize our code so that fail is bound first...

$.get('/users')
  .fail -> showError()
  .done -> showSuccess()

...our AJAX call would appear to work but our success callback would never fire, as it's being bound inside our fail callback which won't be executing unless our AJAX call happens to fail too:

$.get('/users').fail(function() {
  return showError().done(function() {
    return showSuccess();
  });
});

A similar gotcha based on the same error; suppose done wasn't short, but rather quite long:

$.get('/users')
.done (users) ->
  for user in users
    $('#users-table').append("<tr><td>#{user}</td></tr>");
  # lots
  # more
  # stuff
  $('#user-table').on 'click', 'td', ->
    console.log($(this).text())
.fail ->showError()

Now, we might decide to move the fail handler up above the done handler, since it's a single line which is kind of dangling off the end:

$.get('/users')
.fail -> showError()
.done (users) ->
  for user in users
    $('#users-table').append("<tr><td>#{user}</td></tr>");
  # lots
  # more
  # stuff
  $('#user-table').on 'click', 'td', ->
    console.log($(this).text())

Again, we wind up chaining method invocations to the wrong object. We're adding our done callback to the return value of showError().

Solution

Don't put your callbacks on the same line as a done or fail, or wrap them in parenthesis.

Either of the following work fine:

$.get('/users')
  .fail ->
    showError()
  .done ->
    showSuccess()

or...

$.get('/users')
  .fail(-> showError())
  .fail(showError) # drop arrow and supply callback as argument
  .done(-> showSuccess()) # not strictly necessary

As noted, it's not strictly necessary for the last method invocation in a chain, but by omitting it you're leaving yourself open to potential future confusion when you attempt to add another method to the chain.

A short list of CoffeeScript gotcha's I've encountered writing CoffeeScript professionally.

These are not problems with CoffeeScript, rather they are strange one-off situations where, especially coming from Ruby, differences in CoffeeScript's syntax have led to unexpected JavaScript.

Splitting object properties on multiple lines (all or nothing)

##tl;dr

Always this:

user.save
  first_name: 'bob'
  last_name: 'smith'
  login: 'bob.smith'
  email: '[email protected]'
  homepage: 'bobsmith.com'

Never this:

user.save first_name: 'bob', last_name: 'smith', login: 'bob.smith',
  email: '[email protected]', homepage: 'bobsmith.com'

CoffeeScript does something slightly unexpected when you break an "options hash" (really an object) up over multiple lines.

Gotcha

Suppose we're writing a method with accepts the traditional options hash:

user.save first_name: 'bob', last_name: 'smith', login: 'bob.smith', email: '[email protected]', homepage: 'bobsmith.com'

The compiled JavaScript is what we'd expect:

user.save({
  first_name: 'bob',
  last_name: 'smith',
  login: 'bob.smith',
  email: '[email protected]',
  homepage: 'bobsmith.com'
});

But that line is getting a little long. Especially coming from a Ruby background, we may be tempted to simply break the method up into two lines in any of the following ways:

user.save first_name: 'bob', last_name: 'smith', login: 'bob.smith',
  email: '[email protected]', homepage: 'bobsmith.com'

or...

user.save first_name: 'bob', last_name: 'smith', login: 'bob.smith',
          email: '[email protected]', homepage: 'bobsmith.com'

The problem is CoffeeScript has taken our single arguments hash and turned it into two arguments hashes. The "fixed" versions above compile to the following JavaScript:

user.save({
  first_name: 'bob',
  last_name: 'smith',
  login: 'bob.smith'
}, {
  email: '[email protected]',
  homepage: 'bobsmith.com'
});

The email and homepage properties will silently be lost, producing a bug which is both hard to notice and difficult to track down.

The problem also exists if we try to use the traditional method of Ruby hash indentation:

user.save first_name: 'bob',
          last_name: 'smith',
          login: 'bob.smith',
          email: '[email protected]',
          homepage: 'bobsmith.com',

This again gives us two hashes: user.save({first_name: 'bob'}, {last_name: 'smith', login: .... }).

Solution

Either don't indent the second line, or put the entire options hash, one key/value per line, after the method invocation. Either of these produce the desired output:

user.save
  first_name: 'bob',
  last_name: 'smith',
  login: 'bob.smith',
  email: '[email protected]',
  homepage: 'bobsmith.com',

or

user.save first_name: 'bob', last_name: 'smith', login: 'bob.smith',
email: '[email protected]', homepage: 'bobsmith.com'

However the second example introduces it own gotcha. If you miss the trailing , on the first line, the second line is treated like a separate statement, and you get another difficult to detect bug. This is a reasonable thing to miss, given CoffeeScript makes the use of commas optional when declairing objects.

For example:

user.save first_name: 'bob', last_name: 'smith', login: 'bob.smith' # <- no comma
email: '[email protected]', homepage: 'bobsmith.com'

compiles to...

user.save({
  first_name: 'bob',
  last_name: 'smith',
  login: 'bob.smith'
});

({
  email: '[email protected]',
  homepage: 'bobsmith.com'
});

It's also worth noting the odd behaviour of comma-first style here. If we put the comma on the second line instead of leaving it on the first line, the problem vanishes, regardless of the level of indentation of the second line:

user.save first_name: 'bob', last_name: 'smith', login: 'bob.smith'
        , email: '[email protected]', homepage: 'bobsmith.com'

This gives us the desired single hash again:

user.save({
  first_name: 'bob',
  last_name: 'smith',
  login: 'bob.smith',
  email: '[email protected]',
  homepage: 'bobsmith.com'
});

Suggestion

If a line containing an object declaration grows too long, always place keys on subsequent lines, one key/value per line, indented one level:

user.save
  first_name: 'bob'
  last_name: 'smith'
  login: 'bob.smith'
  email: '[email protected]'
  homepage: 'bobsmith.com'

Operator =~ isn't a thing

Not really a CoffeeScript gotcha, but something that could burn a few minutes:

If you forget you're writing CoffeeScript/JavaScript and try to use the =~ operator...

my_string =~ /what/

... it will happily compile (in CoffeeScript) to the following and run (in CS or JS) without errors:

my_string = ~/what/

Instead of testing the contents of my_string, we're applying the bitwise NOT operator,~, to a regular expression, which is coerced to an integer (0) and bitflipped to -1.

In JavaScript:

-1 == ~/abc/; // true
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment