Sunday, September 4, 2011

Error Handling in Backbone.js

‹prev | My Chain | next›

I was able to eliminate the last little oddity in my Backbone.js appointment calendar application last night. At this point I am able to add and delete (though not update) calendar appointments. I am getting to the point that the overall codebase leaves much to be desired. Before I begin refactoring, I notice yet another bug...

If add an appointment to my calendar:

Then save it, all is well:
The appointment shows up on the calendar and persists on reload.

If I don't reload the page and delete the appointment by clicking the "X" icon, it is removed:

If I now reload the page, the appointment is back from the great beyond:

So what gives? A quick check of the error logs reveals that the calendar appointment was created (HTTP 201). But when I tried to delete the record, there was a 409 response from my CouchDB backend:
Got response: 201 localhost:5984/calendar
Got response: 409 localhost:5984/calendar/7acf98778a669f4d6fc33d6b3400e480
Got response: 200 localhost:5984/calendar/_all_docs?include_docs=true
There are, in fact two bugs here. The first is that my Backbone app is not sending the revision number of the newly created appointment when it comes time to delete the record. That is a somewhat understandable oversight on my part. What is not so OK is the lack of error handling that I have built. The frontend responded to the 409 as if nothing went wrong—the appointment was removed from the calendar as if nothing went wrong.

Taking a look at the delete route in my express.js server, I have:
app.delete('/appointments/:id', function(req, res){
  var options = { /* Connection Options */  };

  var couch_req = http.request(options, function(couch_response) {
    console.log("Got response: %s %s:%d%s", couch_response.statusCode, options.host, options.port, options.path);

    couch_response.pipe(res);
  }).on('error', function(e) {
    console.log("Got error: " + e.message);
  });

  couch_req.end();
});
Interesting. I had expected the 409 response from CouchDB to be considered an error by node.js's http.request(). But I am not seeing the "Got error" message logged. I am seeing the "Got response" message:
Got response: 409 localhost:5984/calendar/7acf98778a669f4d6fc33d6b3400e480
Ah, looking at the http.request documentation, I see that:
If any error is encountered during the request (be that with DNS resolution, TCP level errors, or actual HTTP parse errors) an 'error' event is emitted on the returned request object.
The failure here is not a connection error and not technically a parse error, so I suppose that the error event should not be fired after all.

Checking out the Network tab in Chrome's Developer Tools, I see:
Hrm... the response being sent back from the node.js app is a 200 OK:
HTTP/1.1 200 OK
X-Powered-By: Express
Connection: keep-alive
Transfer-Encoding: chunked
Looking at the actual body of the response, however, there clearly was an error:
"error":"conflict","reason":"Document update conflict."}
Well, I have the correct 409 statusCode in the couch_response already. It seems that the solution here is simple enough. I set the HTTP response from my express.js app to be that of the CouchDB response that I am proxying:
  // ...
  var couch_req = http.request(options, function(couch_response) {
    console.log("Got response: %s %s:%d%s", couch_response.statusCode, options.host, options.port, options.path);

    res.statusCode = couch_response.statusCode;
    couch_response.pipe(res);
  }). // ...
Now, when I delete, the response back in the browser is the expected 409:
But that is not quite the end of it. Although the image is no longer removed from the UI, there is no visual indication to the user why this occurred. Clicks on the "X" icon now seemingly have no effect.

Well, the model is receiving the 409 error, but the view needs to be told of the fact. Can it be as easy as subscribing the view to an error event from the model?
    window.AppointmentView = Backbone.View.extend({
      initialize: function(options) {
        this.container = $('#' + this.model.get('startDate'));
        options.model.bind('destroy', this.remove, this);
        options.model.bind('error', this.deleteError, this);
      },
      deleteError: function(model, error) {
        // TODO: blame the user instead of the programmer...
        if (error.status == 409) {
          alert("This site does not understand CouchDB revisions.");
        }
        else {
          alert("This site was made by an idiot.");
        }
      },
      // ...
    });
Yup. It's exactly that easy. Now, when I click delete, an alert pops informing me that I'm an idiot:
That's a good stopping point for tonight. Up tomorrow, I will fix the 409 error itself and (assuming I do not uncover yet another defect) start to refactor a bit.


Day #133

No comments:

Post a Comment