Question

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  # ...
Était-ce utile?

La 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]

Autres conseils

As discussed in Issue #35 of the IcedCoffeeScript repository, there's another technique based on iced-style connectors, which are functions that take as input a callback/deferral, and return another callback/deferral.

Imagine your project has a standard order of arguments to callbacks: the first parameter is always the error, which is null on success. Also, assume further, that you want to leave a function at the first sign of error.

The first step is to make a connector, which I'm calling an "ErrorShortCircuiter" or "ESC":

{make_esc} = require 'iced-error'

Which is implemented like so:

make_esc = (gcb, desc) -> (lcb) ->
    (err, args...) ->
        if not err? then lcb args...
        else if not gcb.__esc
            gcb.__esc = true
            log.error "In #{desc}: #{err}"
            gcb err

To see what this is doing, consider an example of how to use it:

my_fn = (gcb) ->
    esc = make_esc gcb, "my_fn"
    await socket.get 'image id', esc defer id
    await Image.findById id, esc defer image
    await check_permissions user, image, esc defer permitted
    gcb null, image

This version of my_fn first makes an ErrorShortCircuiter (or esc), whose job is twofold: (1) to fire gcb with the error object; and (2) log a message about where the error happened and what the error was. Obviously you should vary the exact behavior based on your setting. Then, all subsequent calls to library functions with callbacks will be given callbacks generated by defer as usual, and then run through the esc connector, which will change the behavior of the callback. The new behavior is to call the gcb global to the function on an error, and to let the current await block finish on success. Also, in the succes case, there's no need to deal with a null error object, so only the subsequent slots (like id, image, and permitted) are filled in.

This technique is very powerful and customizable. The key idea is that the callbacks generated by defer are really continuations and can alter the subsequent control flow of the whole program. And they can do this in a library, so you can get the error behavior you need for many different types of applications, that call upon libraries with different conventions.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top