(Edit: Oops! Sorry, I see that you're already familiar with continuations, but let me explain them for those who aren't)
Sometimes, fundamental switches in thinking are worth it. :)
For example, continuations would be perfect here, and are a wonderful tool for simplifying "nested callback hell" type problems.
But why should a web developer care about continuations?
Consider Paul Graham's Arc challenge: Build a web page that presents a form to the user. After they submit the form, a new page presents them with a link. The link takes them to a third page where the submitted value is presented back to the user. The restriction is that the input field must not be passed in via the URL.
Right now, the user must write in "continuation-passing style", which means threading callbacks, like this:
function serve(req,res) {
askUserForValue(req,res, function(value){
showLink(req, res, function(){
render(res, {"user-value": value});
});
});
}
Or perhaps the user could somehow stuff the variable into a "session" library with three separate URL routes:
function serve(req,res) {
render the form with the POST action of /submit;
}
function submit(req,res, value) {
req.session["uservalue"] = value;
render the link that sends the user to /view;
}
function view(req,res) {
render the value of req.session["uservaule"];
}
This one is even more fragmented since you have three separate functions that model one user interaction flow! I certainly like it less than the callback example -- at least I can keep one in my head.
We can do better though. Continuations let you write this:
function serve(req, res) {
var value = askUserForValue();
showLink();
render(res, {"user-value": value});
}
If node.js supported continuations, the askUserForValue() function would respond with the form and
suspend the serve function right then and there. After the user made a new HTTP call, the suspended askUserForValue() call would return to serve() and its return value would be placed into our variable. Then, the showLink() function could render the link and
suspend the function again. Once the user makes another HTTP request, the function resumes, and so on.
The server would have to store the state of the computation itself to disk so it could continue later.
With continuations, we can pretend the user's web browser is the program counter. This frees my mind up. First, I don't have to keep callbacks in my head since there aren't any callbacks anymore. Second, stack traces work the way I expect because functions explicitly call each other. Third, if askForValue() throws an exception if the user inputs a string where I entered a number, no problem! Just wrap it in a try{} block that asks the user to enter a valid value, as if I'm writing a text program that calls readline().
Also note that this is different from just letting each user interaction be in its own thread! Consider:
1. Continuations should be serializable to disk. You wouldn't want blocked threads to just pile up as more users visit your website, and if your server crashes, you certainly wouldn't want to forget their state.
2. The browser "back" button would have to step a program backwards in time; something that is unsupported in a naïve thread-equals-user-interaction system. (E.g. how many of you have booked an airplane ticket with Travelocity/Expedia/whatever, pressed "Back", and then got a big error because the server didn't know how to step backwards through its user stack?) But the ability to save continuations to disk is the ability to save the current state of the program to disk, which makes stepping back in time easy: you can simply reload what was about to happen, just like savestates in a video game emulator.
This is not a new idea. Many Common Lisp and Smalltack web frameworks work well with continuations and have done so for years (the first Seaside release was 2002, I'm not kidding you!)
Racket, a great dialect of Scheme, ships with such a web framework out of the box. Here's my implementation of the Arc challenge in Racket. Note how the 'start' function is implemented: https://gist.github.com/gcr/ad116f565e105c8b2e0d
It's super easy to try this out and you don't need to fiddle with dependencies or anything. Just download Racket from http://racket-lang.org/ , copy+paste the code into DrRacket's giant text box on the top, press Run, and your web browser will pop up with the example.