3

I'm toying with making something in node.js, and I (like everyone else who's ever started learning node) have a question about the asynchronous nature of it. I searched around a bit, but couldn't find this specific question about it answered (maybe I just didn't search very well...), so here goes:

Are node.js callbacks, in general, guaranteed to be asynchronous if the documentation says so? If you make your own functions that take callbacks, should you design in such a way so that they're either always asynchronous or always synchronous? Or can they be sometimes synchronous, sometimes not?

As an example, lets say you wanted to load some data over the internet, and you created a function to load it. But the data doesn't change very often, so you decide to cache it for future calls. You could imagine that someone would write that function something like this:

function getData(callback) {
    if(dataIsCached) {
        callback(cachedData)
    } else {
        fetchDataFromNetwork(function (fetchedData) {
            dataIsCached = true;
            cachedData = fetchedData;
            callback(fetchedData);
        });
    }
}

Where fetchDataFromNetwork is a function that executes its callbacks asynchronously (this is slightly pseudo-codey, but I hope you understand what I mean).

This function will only fire asynchronously if the data isn't cached, if it is cached it just executes the callback directly. In that case, the asynchronous nature of the function is, after all, totally unnecessary.

Is this kind of thing discouraged? Should the second line of the function be setTimeout(function () {callback(cachedData)}), 0) instead, to guarantee that it fires asynchronously?

The reason I'm asking is that I saw some code a while back where the code inside the callback simply assumed that the rest of the function outside the callback had executed before the code inside the callback. I recoiled a little at that, thinking "but how do you know that the callback will fire asynchronously? what if it doesn't need to and executes synchronously? why would you ever assume that every callback is guaranteed to be asynchronous?"

Any clarification on this point would be much appreciated. Thanks!

Oskar
  • 889
  • 7
  • 20
  • Sometimes one, sometimes the other, if you're referring to 3rd party modules too. They should always be asynchronous, but, not all developers are perfect. – Kevin B Aug 11 '15 at 14:48
  • Callbacks can be either. Promises on the other hand will **always** by asynchronous (so long as they follow the Promise/A+ spec). – idbehold Aug 11 '15 at 14:49
  • Guess it depends on what you consider to be a `callback`. – Kevin B Aug 11 '15 at 15:18

4 Answers4

3

Your assumptions are all correct.

Are node.js callbacks, in general, guaranteed to be asynchronous if the documentation says so?

Yes. Of course, there are functions with async callbacks and functions with sync callbacks, but none which do both.

If you make your own functions that take callbacks, should you design in such a way so that they're either always asynchronous or always synchronous?

Yes.

They could be sometimes synchronous, sometimes not, such as a cache? Is this kind of thing discouraged?

Yes. Very much d̲̭i̫̰̤̠͎͝ͅͅs͙̙̠c̙͖̗̜o͇̮̗̘͈̫ų̗͔̯ŕa҉̗͉͚͈͈̜g͕̳̱̗e҉̟̟̪͖̠̞ͅd͙͈͉̤̞̞̩.

Should the second line of the function be setTimeout(function () {callback(cachedData)}), 0) instead, to guarantee that it fires asynchronously?

Yes, that's a good idea, though in node you'd rather use setImmediate or process.nextTick instead of setTimeout.
Or use promises, which do guarantee asynchrony, so that you don't have to care about delaying things yourself.

I saw some code a while back where the code inside the callback simply assumed that the rest of the function outside the callback had executed before the code inside the callback. I recoiled a little at that

Yeah, understandable. Even if the API you're using guarantees asynchrony, it still would be better to write code so that it can be read in the order it would be executed. If possible, you should place the things that are executed immediately before the async callback (the exception proves the rule).

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
2

Personally I agree with your notion of never assuming asynchronous code execute after the function has returned (by definition asynchronous simply means you can't assume it's synchronous, not assume it's not synchronous).

But there have been a culture that have evolved around javascript that considers a function that can be either sync or async an anti-pattern. And it makes sense: if you can't predict when a code runs it's hard to reason about it.

So in general all the popular libraries avoid it. In general, it's quite safe to assume that async code never runs before end of script.

Unless you have a very special reason for it, don't write a function that can be both sync and async - it's considered an anti-pattern.

slebetman
  • 109,858
  • 19
  • 140
  • 171
  • Although I personally share this view, I'd love to see some citations and references to back it up. – georg Aug 11 '15 at 15:00
  • Spent the last 10 minutes googling the discussion I had with the promises/A people back when I complained that promises should also handle synchronous code. There's even a couple of blog post about never mixing sync/async. But can't find any (sync/async is suddenly a very popular topic on google). That's about as much time as I'd like to invest in an answer. – slebetman Aug 11 '15 at 15:03
  • @georg, slebetman: You're probably looking for http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony. – Bergi Aug 11 '15 at 16:10
0

You should never have a function that accepts a callback that is sometimes synchronous, this can cause you to run into tail recursion problems. Take the following example:

function doSynchronousWork (cb) {
    cb();
}
doSynchronousWork(function looper () {
   if (...somecondition...) {
       doSynchronousWork(looper);
   }
});

In the above example, you'll run into a max callstack exceeded error due to the callstack nesting too deeply. Forcing the synchronous function to run asynchronously fixes that problem by clearing the callstack before continuing the recursion.

function doSynchronousWork (cb) {
    process.nextTick(cb);
}
doSynchronousWork(function () {
   if (...somecondition...) {
       doSynchronousWork();
   }
});

Note that this tail recursion problem will be fixed eventually in the js engine

Do not confuse callbacks with iterator functions

Kevin B
  • 94,570
  • 16
  • 163
  • 180
0

You need to free yourself of the notion that something doesn't need to be asynchronous - in Node either something needs to be synchronous, or it should be asynchronous without another thought.

Async is the entire philosophy behind Node.js, and callbacks are by their very nature asynchronous (if they're designed properly), which allow Node to be non-blocking as it claims. This is what predicates the assumption that a callback will execute asynchronously - it's an intimate part of Node's design philosophy.

Should the second line of the function be setTimeout(function () {callback(cachedData)}), 0) instead, to guarantee that it fires asynchronously?

In Node we use process.nextTick() to ensure something runs Asynchronously, delaying it's function until the next tick of the event loop.

jonny
  • 3,022
  • 1
  • 17
  • 30
  • 1
    `callbacks are by their very nature asynchronous`.. no, asynchronous functions by their nature require callbacks but callbacks are not by their nature async. Lots of examples of sync callbacks exist even in node: Array.prototype.forEach(), Array.prototype.map() etc. – slebetman Aug 11 '15 at 15:12
  • @slebetman I wouldn't consider those callbacks. though that terminology is a bit confusing, or conflicted in the javascript world atm. I'd consider them iterator functions, or comparator functions. – Kevin B Aug 11 '15 at 15:15
  • @slebetman as Kevin B pointed out, they are examples of iterators. It's a necessary distinction to make. – jonny Aug 11 '15 at 15:22
  • 1
    Then there's the fact that this question is asking about a specific case in which having it be sometimes synchronous can cause real problems – Kevin B Aug 11 '15 at 15:25
  • @KevinB: Iterators are something different - they in fact also exist in es6. What we call "callbacks" are technically higher order functions - otherwise known as functional programming. "Callbacks" proper are a subset of higher order functions that operate on asynchronous data - otherwise known as I/O monads. But the js community have gotten used to calling them callbacks. Note that the specification of Array.prototype.forEach itself calls it callback: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach. Do not confuse callbacks with iterators. – slebetman Aug 11 '15 at 16:22
  • MSDN also call it callback: https://msdn.microsoft.com/en-us/library/ff679980(v=vs.94).aspx. Also the ES5 spec calls it a callback: https://people.mozilla.org/~jorendorff/es5.html – slebetman Aug 11 '15 at 16:23