node.jserror-handlingcoffeescripticed-coffeescript

How to correctly handle errors with IcedCoffeeScript?


It is common practice in node.js to return error message as the first argument to a callback function. There are a number of solutions to this problem in pure JS (Promise, Step, seq, etc), but none of them seem to be integrable with ICS. What would be correct solution to handle errors without losing much of readability?

For example:

# makes code hard to read and encourage duplication
await socket.get 'image id', defer err, id
if err # ...
await Image.findById id, defer err, image
if err # ...
await check_permissions user, image, defer err, permitted
if err # ...


# will only handle the last error
await  
  socket.get 'image id', defer err, id
  Image.findById id, defer err, image
  check_permissions user, image, defer err, permitted

if err  # ...


# ugly, makes code more rigid
# no way to prevent execution of commands if the first one failed
await  
  socket.get 'image id', defer err1, id
  Image.findById id, defer err2, image
  check_permissions user, image, defer err3, permitted

if err1 || err2 || err3  # ...

Solution

  • I solve this problem through style and coding convention. And it does come up all the time. Let's take your snippet below, fleshed out a little bit more so that we have a workable function.

    my_fn = (cb) ->
      await socket.get 'image id', defer err, id
      if err then return cb err, null
      await Image.findById id, defer err, image
      if err then return cb err, null
      await check_permissions user, image, defer err, permitted
      if err then return cb err, null
      cb err, image
    

    You're exactly right, this is ugly, because you are short-circuiting out of the code at many places, and you need to remember to call cb every time you return.

    The other snippets you gave yield incorrect results, since they will introduce parallelism where serialization is required.

    My personal ICS coding conventions are: (1) return only once from a function (which control falls off the end); and (2) try to handle errors all at the same level of indentation. Rewriting what you have, in my preferred style:

    my_fn = (cb) ->
      await socket.get 'image id', defer err, id 
      await Image.findById id, defer err, image                   unless err?
      await check_permissions user, image, defer err, permitted   unless err?
      cb err, image
    

    In the case of an error in the socket.get call, you need to check the error twice, and it will obviously fail both times. I don't think this is the end of the world since it makes the code cleaner.

    Alternatively, you can do this:

    my_fn = (autocb) ->
      await socket.get 'image id', defer err, id
      if err then return [ err, null ]
      await Image.findById id, defer err, image
      if err then return [ err, null ]
      await check_permissions user, image, defer err, permitted
      return [ err, image ]
    

    If you use autocb, which isn't my favorite ICS feature, then the compiler will call the autocb for you whenever you return/short-circuit out of the function. I find this construction to be more error-prone from experience. For instance, imagine you needed to acquire a lock at the start of the function, now you need to release it n times. Others might disagree.

    One other note, pointed out below in the comments. autocb works like return in that it only accepts one value. If you want to return multiple values as in this example, you need to return an array or dictionary. defer does destructuring assignments to help you out here:

    await my_fn defer [err, image]