Using jQuery Deferred To Chain Validation Rules In An Asynchronous, Non-Blocking Environment
A while back, I blogged about how client-side validation is changing the structure of server-side validation; with the client creating user-friendly error messages, the server can finally streamline its validation and data processing using a light-weight, exceptions-based workflow. While I really like this evolutionary step in data handling, I realize that it has some limitations. As I was playing with Node.js on Amazon's EC2 web service, it occurred to me that when validation is done in an asynchronous environment, like Node.js, "throwing exceptions" becomes significantly less useful. In such a case, I think it might make more sense to start chaining Deferred validation steps using promises.
With the burden of creating "user friendly" error messages off-loaded to the client in a thick-client application, the server-side data processing can focus more on efficiency and less on the user. Rather than creating an aggregate of errors, the server can simply stop processing the moment it comes across any data that is considered invalid.
In essence, the server can use a "short-circuit" approach to validation, processing data only up until a failure is eminent:
if (item 1 is invalid) throw error....
if (item 2 is invalid) throw error....
if (item 3 is invalid) throw error....
if (item 4 is invalid) throw error....
if (item 5 is invalid) throw error....
This works quite well in a synchronous, blocking environment like ColdFusion; in an asynchronous environment like Node.js, however, throwing errors may or may not actually affect the primary control flow of the request. If part of the data needs to be validated in an asynchronous branch (ex. getting data from a NoSQL database), any exception raised will fall outside the control flow of a top-level, synchronous Try/Catch block.
To adapt to this new, non-blocking environment, I think we have to move away from an exceptions-based validation framework and into a Deferred-based validation framework. In the past, I've started to talk about using jQuery's pipe() method to chain asynchronous validation events; and, I think that's moving in the right direction. My concern with using pipe() is simply that it feels a little too unstructured. I think we can take the pipe() philosophy and encapsulate it behind a workflow that is geared more specifically to chaining dependent, asynchronous processing steps.
If you can imagine that each one of my IF() statements from the above validation pseudo-code is asynchronous, I'd like to create a new workflow that looks the same, but builds off of the concept of Deferred results rather than exceptions:
if (item 1 is invalid) reject()....
if (item 2 is invalid) reject()....
if (item 3 is invalid) reject()....
if (item 4 is invalid) reject()....
if (item 5 is invalid) reject()....
Here, each asynchronous step can either reject() or resolve() itself. Resolving a step will allow the next step to be proceed; rejecting a step, on the other hand, will short-circuit the process, halting the flow of validation.
To experiment with this approach, I created a jQuery plugin called $.validate(). validate() can take N number of function arguments, each of which will be invoked in serial - and, only as necessary as defined by a short-circuit approach to validation. When invoked, each function will be passed a Deferred instance that it can either resolve() or reject().
The validate() function will, itself, return a Deferred Promise object that allows then(), done(), and fail() binding. At this level, done() will represent a fully-validated data set; fail() will indicate that at least one aspect of the data is invalid. You can think of the fail() binding as being analogous to the Catch block within our synchronous, blocking approach.
demo.js - An Experiment In Short-Circuit, Asynchronous Validation
// Define the aynchronous validate() plugin for jquery.
(function( $ ){
// Define the validation factory. This will create an augmented
// Deferred object for each validator within the validation chain.
function Validation(){
// Create a deferred object that will be used to hold the
// state of a given validation step.
var deferred = $.Deferred();
// Define the convenience methods for semantically meaningful
// rejection states.
// The data submitted with the request is bad (in part).
deferred.badRequest = function( message ){
// Reject the deferred with a 400 response.
deferred.reject( 400, (message || "Bad Request") );
};
// The user is not authorized to make the request.
deferred.notAuthorized = function( message ){
// Reject the deferred with a 401 response.
deferred.reject( 401, (message || "Not Authorized") );
};
// The target of the request could not be found.
deferred.notFound = function( message ){
// Reject the deferred with a 404 response.
deferred.reject( 404, (message || "Not Found") );
};
// The request method was not allowed for this call.
deferred.methodNotAllowed = function( message ){
// Reject the deferred with a 405 response.
deferred.reject( 405, (message || "Method Not Allowed") );
};
// Return the augmented deferred object.
return( deferred );
}
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// Define the public API for the validation plugin.
$.validate = function( /* callbacks */ ){
// Create a master deferred object for the entire validation
// process. This will be rejected if ANY of the validators
// is rejected. It will be resolved only after ALL of the
// validators is resolved.
var masterDeferred = $.Deferred();
// Create a true array of validation functions (so that we
// can make use of the core Array functions).
var validators = Array.prototype.slice.call( arguments );
// I provide a recursive means to invoke each validator.
var invokeValidator = function( validator, previousResults ){
// Create a deferred result for this validator.
var result = Validation();
// Create a promise for our deferred validation so that
// we can properly bind to the resolve / reject handlers
// for the validation step.
result.promise().then(
function( /* Resolve arguments. */ ){
// This validation passed. Now, let's see if we
// have another validation to execute.
var nextValidation = validators.shift();
// Check for a next validation.
if (nextValidation){
// Recusively invoke the validation. When we
// do this, we want to pass-through the
// previous validation result in case it is
// needed by the next step.
return(
invokeValidator( nextValidation, arguments )
);
}
// No more validation steps are provided. We can
// therefore consider the validation process to
// be resolved. Resolve the master deferred.
masterDeferred.resolve();
},
function( type, message ){
// This validation failed. We cannot proceed with
// any more steps in validation - we must reject
// the master deferred.
// Check to see if we have any arguments.
if (arguments.length === 0){
// Since we have no data, we have to default
// to a 500 erorr.
type = 500;
message = "Internal Server Error";
// Check to see if we have two arguments - if not,
// then we can fill in the TYPE.
} else if (arguments.length === 1){
// Shift message to appropriate param.
message = type;
// Set 400 error message since we have an
// error message, but we don't know what type
// of error it was exactly.
type = 400;
}
// Reject the master deferred.
masterDeferred.reject( type, message );
}
);
// While the validation is intended to be asynchronous,
// let's catch any synchronous errors.
try {
// Create an invocation arguments collection so that
// can seamlessly pass-through any previous result.
var validatorArguments = (previousResults || []);
// Prepend the result promise onto the array.
Array.prototype.unshift.call(
validatorArguments,
result
);
// Call the validator.
validator.apply( null, validatorArguments );
} catch( syncError ){
// If there was a synchronous error in the callback
// that was not caught, let's return a 500 server
// response error.
masterDeferred.reject(
500,
(syncError.type || "Internal Server Error")
);
}
};
// Invoke the first validator.
invokeValidator( validators.shift() );
// Return the promise of the master deferred object.
return( masterDeferred.promise() );
};
})( jQuery );
// End jQuery plugin.
// ---------------------------------------------------------- //
// ---------------------------------------------------------- //
// ---------------------------------------------------------- //
// ---------------------------------------------------------- //
// Set up a dummy http object for our validation.
var HTTP = {
method: "PUT",
authorization: "ben"
};
// Set up a dummy form object for our validation.
var form = {
id: 4,
name: "Tricia",
age: "20",
email: "tricia-smith@gmail.com"
};
// ---------------------------------------------------------- //
// ---------------------------------------------------------- //
// Run the validation on the request.
var requestValidation = $.validate(
// Validate that the incoming request has been authorized for
// this API request.
function( validation ){
// Check to see if the given user is authorized.
if (HTTP.authorization !== "ben"){
// Reject the request.
return( validation.notAuthorized() );
}
// Approve this STEP of the validation process.
validation.resolve();
},
// Make sure this was a PUT since we are updating data.
function( validation ){
// Check for PUT verb (required for this update).
if (HTTP.method !== "PUT"){
// Reject the request.
return( validation.methodNotAllowed( "Updates require PUT." ) );
}
// Approve this STEP of the validation process.
validation.resolve();
},
// Validate ID of target user.
function( validation ){
// PRETEND that this step was actually something that
// required going to the disk / databse asynchronously.
setTimeout(
function(){
// For this step, just assume it was found.
if (false){
// Reject the request.
return( validation.notFound( "User was not found." ) );
}
// Approve this STEP of the validation process.
validation.resolve();
},
1000
);
},
// Validate the name.
function( validation ){
// Check to see that the name has a valid length.
if (!$.trim( form.name ).length){
// Reject the request.
return( validation.badRequest( "Name is required." ) );
}
// Approve this STEP of the validation process.
validation.resolve();
},
// Validate age data type (for numeric).
function( validation ){
// Make sure the age is numeric.
if (isNaN( form.age )){
// Reject the request.
return( validation.badRequest( "Age must be a number." ) );
}
// Approve this STEP of the validation process and pass the
// AGE value through to the next step. We don't need to do
// this, really - I'm just testing the feature.
validation.resolve( form.age );
},
// Validate that the age number is appropriate.
function( validation, age ){
// Make sure the age at least 18.
if (age < 18){
// Reject the request.
return( validation.reject( "Age must be GTE to 18." ) );
}
// Approve this STEP of the validation process.
validation.resolve();
},
// Validate the email address.
function( validation ){
// LOSELY check for email format.
if (form.email.search( /^[^@]+@[^@]+\.\w+$/ ) === -1){
// Reject the request.
return( validation.reject( "Valid email address is required." ) );
}
// Approve this STEP of the validation process.
validation.resolve();
}
);
// Bind to the outcome of the request validation. If all of the steps
// have passed validation then the request is valid and can be
// processed further. If ANY OF THE STEPS were rejeceted, the request
// is NOT valid.
requestValidation.then(
// Success handler.
function(){
// Log success!
console.log( "SUCCESS!" );
},
// Fail handler (for validation).
function( type, message ){
// Log error!
console.log( "FAIL", type, message );
}
);
The first half of this code is the actual jQuery plugin, validate(). The second half is a mock test of the plugin. I've created an HTTP and form object to hold test data. Then, I invoked the validate() plugin, providing a function callback for each step of the validation process. If any of the steps fail, the succeeding callbacks will not be invoked.
You'll notice that in addition to the core reject() and resolve() methods, I've augmented the Deferred objects to provide HTTP-status-code-oriented class methods:
- Deferred.badRequest()
- Deferred.notAuthorized()
- Deferred.notFound()
- Deferred.methodNotAllowed()
Since this is meant to traslate a server-side data validation workflow into an asynchronous environment, I wanted to keep reject() methods that were in alignment with the types of Exceptions that would be raised in a synchronous, blocking validation workflow. Each rejection method correlates to a status code and error message that is ultimately passed to the fail() handler on the top-level Deferred object. If the core reject() method is used on any given step, it is translated into a 500 status code in the fail() handler.
This approach is intended to be used in an asynchronous environment like Node.js. Unfortunately, jQuery is tightly bound to a DOM-based environment (like your browser) and doesn't necessarily play well with server-side JavaScript environments that know nothing about a Document Object Model (DOM). As such, while I think this approach feels good, it would ultimately have to be built on different or extracted technologies.
Want to use code from this post? Check out the license.
Reader Comments
Just a quick tip, if you want to play with Node.JS without having to deal with setting up an EC2, http://www.C9.io lets you write, build, and run node.js apps completely within the browser. It even has a built in console for installing modules, i.e.
@Jonathan,
I have to say that Cloud9 looks pretty awesome! I'll have to investigate further.
One of my biggest concerns right now, with Node.js, is deployment in general. I think the "cool" move is to deploy to GitHub and then to "pull/push" from GitHub to production server. I don't know enough about that workflow yet.
It looks like Cloud9 can work perhaps WITH this approach and in lieu of it as well. Thanks for the link!
The "console" at the bottom of the C9 IDE has GIT integrated. You can use GitHub, or any other GIT repository. When you create a project in C9, it tells you how to setup GIT. Also, you can instantly create projects from your own GitHub account, fork, push/pull all day and night! The editor on GitHub is based on ACE, which is the editor inside Cloud9. As far as deployment/workflow goes, you can also click one button from C9 to deploy to Windows Azure, Heroku, etc. It's kind of epic.
My only complaint for C9 is that I wish it wasn't just optimized for Node. The Node movement is fun, but it's not entirely practical in many situations. C9 supports a ton of other languages (even CF) but you don't get that integrated online environment.
@Jonathan,
It really does sound "epic". I'll try to play around with it. If nothing else, it sounds like it will help me get more comfortable with a lot of deployment-oriented workflow items (which is always good!).
@All,
I tried factoring out the core concept here - chaining asynchronous methods - into it's own jQuery plugin, $.whenSync():
www.bennadel.com/blog/2326-jQuery-whenSync-Plugin-For-Chaining-Asynchronous-Callbacks-Using-Deferred-Objects.htm
This works basically like the core $.when() method, accept that it defers method invocation to the whenSync() plugin rather than passing-through Deferred objects.