Lately, I've been playing around with Node.js on Amazon EC2 (Elastic Compute Cloud). Coming from a ColdFusion background, moving into an asynchronous event loop can be challenging. Simple things like form processing can become complex workflows when even one step of Validation or Processing needs to be performed asynchronously. I've been trying to hone my asynchronous skills in a jQuery context where I can use jQuery Deferred objects to wrangle asynchronous processes. Last week, I created a jQuery plugin, jQuery.whenSync() which can be used to chain asynchronous tasks in serial; this week, I wanted to use that plugin to further explore form validation and processing in an asynchronous context.
To frame the conversation, let me just sum up my current synchronous FORM validation and processing workflow. As the client-side of web applications has become much more robust and feature-rich, we can start to create server-side code that is much more API-oriented. For this, I use an Exceptions-based workflow that makes multi-step processing extremely easy (pseudo-code):
try --- if (field is invalid) Throw exception... --- if (field is invalid) Throw exception... --- if (field is invalid) Throw exception... --- if (field is invalid) Throw exception... --- processData() catch( ExceptionType == 1 ) catch( ExceptionType == 2 ) catch( ExceptionType == 3 ) generateHTTPResponse()
In ColdFusion, this is easy since everything in ColdFusion (less CFThread) is blocking by default. Therefore, if any of the IF statements or data processing steps throws an error, it can be caught by one of the top-level Catch statements.
In jQuery or Node.js, where functionality may be executed in parallel, catching errors becomes much more complex. Luckily, jQuery has the Deferred object which allows for success-and-fail-based asynchronous processing. Taking the above pseudo-code and translating it over to use jQuery Deferred, we can get something that looks like this (pseudo-code):
whenSync() --- if (field is invalid) reject( exception... ) --- if (field is invalid) reject( exception... ) --- if (field is invalid) reject( exception... ) --- if (field is invalid) reject( exception... ) --- processData() fail --- catch( ExceptionType == 1 ) --- catch( ExceptionType == 2 ) --- catch( ExceptionType == 3 ) always --- generateHTTPResponse()
Here, rather than using jQuery.when() to allow all given callbacks to execute in parallel, I'm using the jQuery.whenSync() plugin to make sure asynchronous callbacks are executed in serial. Then, I'm moving the "catch" statements to the fail() callback. And, finally, I'm moving the HTTP Response generation to the always() callback where it can be executed whether or not the request validation and processing succeeded or failed.
Now that we understand the translation of synchronous validation and processing onto an asynchronous platform, let's take a look at some demo code. In the following demo, I am processing a fake FORM submission - validating each field, processing the data, and then generating a fake HTTP response - all in a workflow that plays nicely with asynchronous methods.
As you can see from the code, we are making use of jQuery Deferred methods: fail() and always(). We are not making use of the done() method! This is an important choice since our form "processing" may throw errors even after the data has been deemed valid (think database failure?). As such, we want our fail() method to handle more than just the validation aspects - we want it to catch errors raised through the entire workflow. This requires us to put our traditional "done" code into the last step of the jQuery.whenSync() callbacks.
When we run the above code, we get the following fake HTTP response in our console:
To see the "Catch" portion of the code in action, take a look at the above video.
My understanding of server-side processing is always changing. Even when I get to something that I am quite happy with, I read something like the REST API Design Rulebook by Mark Masse, and realize that my approach needs to be refined even further. Take that journey and put it in an asynchronous context like jQuery or Node.js, and suddenly, you're re-learning how to walk. Luckily, with objects like Deferred and plugins like jQuery.whenSync(), validation and processing practices can, more or less, be translated over with just a little tweaking.