You need to try Erlang. (Or Haskell, but Erlang is more approachable.) You and every other Node.js partisan keep making criticisms that simply make it clear that you have no clue what you are criticizing and that doesn't make it terribly likely that you're going to sway me to your point of view.

"With a callback-based system, stacks are short, and you explicitly carry that state. You KNOW what state you're storing, and you can see it easily."

Actually, no. You have implicit state carried around in the function closures and you will discover that it is very easy to have a leak in there that will be very hard to diagnose. I say this because I am speaking from experience.

(Remember, Node.js isn't a blinding new architecture. The architecture has been around for over a decade and I don't even know when the idea started. The only new aspect is that this time, it's Javascript. I've got a lot of experience with that architecture, and what that experience tells me is never again!)

On the other hand, it is very easy to examine an entire running Erlang system, see every process and the exact contents of its stack at that point in time, and the exact amount of memory currently allocated to it, and since Erlang doesn't have any sharing between processes, that state is everything about that process. It isn't always the best about giving back memory if you have long running processes, but I was able to diagnose which processes were consuming my RAM, determine why they were consuming my RAM, and test out a fix for the excessive RAM consumption (since it came in the form of sending a particular message sooner rather than later), all without shutting down my server.

You do not have that level of introspection and visibility in Node.js. I don't even have to ask.

"Many languages and many runtimes allocate stacks in megabytes, and store every local variable in it."

I'm not talking about "many languages". I don't care about "many languages". I'm talking about good languages. Erlang can very easily allocate a couple hundred bytes to a process, or less. I'm not actually sure what the minimum allocation is, but it is certainly going to be competitive with a minimal Node.js handler.

"Receive a starting event, emit a done event -- you aggregate processes into sets of events, not into function calls. So yeah, it's not going to follow some of the same patterns that non-event-driven code follows, and event-driven code is going to look rather different than callback-passing code."

None of that appears to have any relationship to coding in Erlang, from what I can see. Better technologies don't have to have events. They just code right through things. A loop for a simple proxy might look like:

proxy(SenderSocket, DestSocket) ->
case socket_read(Sender) of
done -> done; %% return to the original caller
{ok, Data} ->
socket_write(DestSocket, Data),
%% go back for more
proxy(SenderSocket, DestSocket);
{error, Error} ->
handle_error(Error)
end
end.

I don't need to "aggregate events"; I just tell the system what I want it to do, and it does it, and I don't sit here and specify how to wire functions together. In Erlang, the above will not block any other process. If you don't want it to block your current process, that's easy:

spawn(fun () -> proxy(SomeSender, SomeDest))

Bam. Separate process and the current process can move on with life. No hooking up events. No code blathering on about how to interleave the events in that process with the events in this process. It's just happening. (There is standard library code to make things even more reliable, but going into the built-in supervisor stuff would take too much time. Also, it's hitting below the belt, no other language has anything quite like OTP.)

Erlang doesn't actually use coroutines, Haskell does only upon request, coroutines for concurrency are just cooperative multitasking and I mock them as well, albeit for different reason.

You need to try Erlang. If only to know how to argue against it without arguing against some fictional language that doesn't exist.