And don't say "monad transformer stacks" somehow solve this problem in an easier way.
I find the "oh you changed tiny piece of logic, you should totally have to refactor every line of code that follows it" very strange. It obviously sucks, there are far better ways to handle it and they are slowly making it into the language.
You get the performance benefits of non-blocking code.
The simplicity benefits of blocking code.
And (almost) none of the threading hell you get in imperative languages.
This is some documentation of the scheduler: http://blog.ezyang.com/2013/01/the-ghc-scheduler/
var a = await someBigFunc()
a <- someBigFunc()
a <- doFirstThing()
b, c <- doSecondThing(a)
doFinalThing(b + c)
You never change code in real life? You don't always know what functions are going to be async up-front. They might become async later, 4 levels down, when someone decides to do some IO. What happens then? Exactly. You have to ripple that code all the way up to the top of the API.
And there isnt some magical point at which a change becomes large enough that it warrants a refactoring of unrelated code just because of the control flow.
Consider the simple case where one wants to look up a value synchronously if one has it in memory, and go get it from storage asynchronously if one doesn't. That's a natural thing to want, but it's problematic in Node. The problem is not syntactic—you can easily write a function that calls back immediately in the one case and asynchronously in the other. It's that sync and async semantics don't compose well, so when you start to do anything a little complicated (e.g. launch N requests and call back when all N have either returned or failed), the two trip over one another. Working in a Lisp that compiles to JS, I had to write some surprisingly complex macros in order to get correct behaviour. I wouldn't dream of writing that JS code by hand.
If every function is effectively impure, it still sucks.
The big problem with callbacks is that they hold dynamic state and behaviour but, unlike other dynamic (and many static) objects in most languages, do not offer any interfaces to manipulate and reason about them. That's what higher level abstractions like promises provide.
First of all, why would you even need to reason about a function's internal state? That's a sign of a leaky abstraction.
Furthermore, every time you want to manipulate the internal state of a function, what you're really after is defining a better API to provide arguments to said function.
And if you still for some reason need to dick around with a function's internal state, just use partial function application.
If all you have to implement async operations are plain callbacks then yeah, they are not really an abstraction of anything, and you can probably call them 'leaky'. Which is why you need to create real abstractions around them, like promises.
I think we basically agree.