Model / can-connect / superMap common error handling

Is there a way to have a “global” error handler in a Model / superMap?

I guess an option would be to sub-class and use that; or is there an easier option?

Depends on the error. If you just want to know when some ajax request went wrong, you can use a jQuery ajax prefilter.

Cool beans! Thanks… I guess that makes sense :slight_smile:

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

Hi Bajix,

Thanks for your input. I’ll give it a better look once I get time. I agree with you i.r.o. global error handling but I use that more as a catch-all than anything specific. Perhaps I’ll have some indicator to bypass global handling when required, etc. Haven’t gotten there yet.

I use an ASP.NET Web-Api so I don;t have middleware running on node.

Regards,
Eben