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]