Globally handling for these sort of things are ineffective. They’re almost exclusively server side validation issues, or otherwise could be handled contextually within your markup.
The approach I take is to make my API validation errors symmetrical in format with my client-side validation errors. If you’re using StealJS, you can use something like the validator to do your validation both on your can.Map, and within your models via mongoose-validator. You can even go a step further use express-validator within your middleware.
I wrote express-error-funnel expressly for combining errors from express-validator / mongoose-validator / mongoose all into the same format.
Once you have uniform error handling, it’s straight forward to merge the two:
errors: {
value: Object,
get: function( map ) {
var validationErrors = this.attr('validationErrors'),
ajaxErrors = this.attr('ajaxErrors'),
params = {};
can.extend(params, validationErrors.attr());
can.extend(params, ajaxErrors.attr());
map.attr(params, true);
return map;
}
},
ajaxErrors: {
value: Object
},
validationErrors: {
value: Object
}
Here I’m deconstructing my ajax errors:
submit: function( promise ) {
var scope = this;
scope.attr({
requestTime: new Date(),
isPending: true,
code: undefined,
statusCode: undefined,
message: undefined
});
return promise.then(function() {
scope.attr('isPending', false);
}, function( xhr ) {
var res = xhr.responseJSON || {},
errors = res.errors || [],
ajaxErrors = {},
params = {};
params.statusCode = params.code = xhr.status;
params.message = xhr.statusText;
for (var i = 0; i < errors.length; i++) {
var error = errors[i];
if (error && error.hasOwnProperty('param')) {
var key = error.param,
msg = error.msg;
ajaxErrors[key] = ajaxErrors[key] || [];
ajaxErrors[key].push(msg);
}
}
[
'code',
'statusCode',
'message'
].forEach(function( key ) {
if (res.hasOwnProperty(key)) {
params[key] = res[key];
}
});
params.statusCode = res.statusCode;
scope.ajaxErrors.attr(ajaxErrors, true);
scope.attr(params);
scope.attr('isPending', false);
});
}
Toss in some helpers to iterate through these errors:
helpers: {
hasNotice: function( options ) {
var errors = this.attr('errors'),
statusCode = errors.attr('statusCode'),
message = errors.attr('message');
if (message && statusCode !== 400) {
return options.fn(message);
}
return options.inverse();
},
isSubmittable: function( options ) {
var validationErrors = this.attr('validationErrors');
if (can.Map.keys(validationErrors).length) {
return options.inverse();
}
return options.fn();
},
isValid: function( key, options ) {
if (this.attr('isDirty')) {
if (typeof key === 'function') {
key = key();
}
var errors = this.errors.attr(key);
if (errors) {
return options.inverse({
errors: errors
});
}
}
return options.fn();
},
isInvalid: function( key, options ) {
if (this.attr('isDirty')) {
if (typeof key === 'function') {
key = key();
}
var errors = this.attr('errors').attr(key);
if (errors) {
return options.fn({
errors: errors
});
}
}
return options.inverse();
}
}
The rest can be accomplished in the markup:
// handle specific cases
{{#is statusCode 404}}
<resource-not-found/>
{{/is}}
// You can build components to essentially trigger some sort of behavior as well
{{# is statusCode 401}}
// VM init -> can.route.attr(this.serialize());
<route-url screen="profile" action="authenticate"/> // redirect back to login.
{{/is}}
//
{{#hasNotice}}
<div class="u-1 has-error box-2 align-center">
<span class="notice">{{.}}</span>
</div>
{{/hasNotice}}
//
<div class="fieldset">
<input type="text" placeholder="Username" {($value)}="user.username"/>
{{#isInvalid 'username'}}
<span class="form-icon -incorrect"><i class="icon-times-circle-o"></i></span>
{{#errors}}
<p class="error">{{.}}</p>
{{/errors}}
{{else}}
{{#if user.username}}
<span class="form-icon -correct"><i class="icon-check-circle-o"></i></span>
{{/if}}
{{/isInvalid}}
</div>
// Use call expressions to pass in a tracking promise from save
<button class="button u-1" ($click)="submit(page.save())">Save</button>
Part of the beauty of this sort of methodology is that you seamlessly capture errors that couldn’t be handled before saving to the database. Something like a unique field for instance, really shouldn’t ever be verified before saving (exception: gamification), and is easiest handled by a unique/compound unique index on the DB level. Something previously tedious like “this slug is already used” is suddenly trivial.
Anyway: TLDR;
Global error handling is insane
Treating validation & error handling similarly makes life good
Love the DOM - use components to trigger business behavior when applicable