1. 6
    1. 4

      Java is a language, while Node is a runtime. Node should be compared against the JVM because each platform can be targeted by different languages. For example, I can target both Node and the JVM with Clojure. In that scenario the problems regarding locking threads don’t exist because Clojure is designed to be thread safe and it provides tools, such as atoms, for working with shared mutable state.

      My experience targeting both the JVM and Node, is that the JVM provides a much simpler mental model for the developer. The JVM allows you to write predominantly synchronous code, and the threads are used to schedule execution intelligently ensuring that no single chunk of code hogs the CPU for too long. With Node you end up doing scheduling by hand, and it’s your responsibility to make sure that your code isn’t blocking the CPU.

      Here’s a concrete example from a script I ended up writing on Node:

      (defn post-status-with-images
        ([status-text urls]
         (post-status-with-images status-text urls []))
        ([status-text [url & urls] ids]
         (if url
           (.get (if (string/starts-with? url "https://") https http) url
                 (fn [image-stream]
                   (post-image image-stream status-text #(post-status-with-images status-text urls (conj ids %)))))
      (post-status status-text (not-empty ids)))))

      here’s what the JVM equivalent would look like:

      (defn post-status-with-images [status-text urls]
        (future (post-status status-text (map post-image urls))))

      You could use promises or async to make the Node example a bit cleaner, but at the end of the day you’re still doing a lot more manual work and the code is more complex than it would’ve been with threads.

      1. 1

        Couldn’t this be better described as a limitation of the implementation Clojure on Node and not actually node?

        1. 2

          I don’t really see how that’s the case. The problem I’m describing is that Node has a single execution thread, and you can’t block it. This means that the burden of breaking up your code into small chunks and coordinating them is squarely on the developer.

          As I said, you could make the code a bit more concise, but the underlying problem is still there. For example, I used promises here, but that’s just putting on a bandaid in my opinion.

          Threads are just a better default from the developer perspective, and it’s also worth noting that you can opt into doing async on the JVM just fine if you really wanted to. It’s not a limitation of the platform in any way.

          1. 1

            Threads are just a better default from the developer perspective

            There is the caveat that threads (at last in the JVM) dramatically increase the complexity of the memory model and are generally agreed to make it harder to write correct code. Single threaded event-loop style programs don’t remove the chance of race conditions and dead locks but they remove a whole class of issues. Personally, I like something like the Erlang model which is fairly safe and scales across hardware threads. My second personal preference is for a single threaded event-loop (although I generally use it in Ocaml which makes expressing the context switches much more pleasant than in JavaScript/Node).

            1. 1

              The part about it being harder to write correct code only applies to imperative languages though. This is why I’m saying that it’s important to separate the platform from the language. I like the Erlang model as well, however shared nothing approach does make some algorithms trickier.

              Personally, I found Clojure model of providing thread safe primitives for managing shared mutable state to work quite well in practice. For more complex situations the CPS model such as core.async or Go channels is handy as well in my experience.