Catching errors on AJAX calls with CakePHP

In many of todays web applications, AJAX requests have a crucial role in the usability. ou don’t want users to have to refresh the page in order to get the data they requested, especially not if it’s in the middle of a form. However, I often see bad error handling practices when it comes to AJAX requests (read: no error handling at all).

Today, I want to share a method with you that I use in my CakePHP projects. The key is in using Cake’s CakeResponse class, to throw an error and have your AJAX call catch it and run the proper action in response. In my case, I had an AJAX method in my InvoiceLinesController to allow users to add a line to an invoice by using an AJAX call (through a jQuery UI dialog). It essentially does this:

<?php
public function ajax_add() {
    // Save the data passed in the POST request
    $this->InvoiceLine->create();
    $this->InvoiceLine->save($this->request->data);
}

Pretty simple, right? Just take in the passed data and save it as an invoice line. Now, this works well if the user enters proper data. But what if the user enters bad data into the form that doesn’t pass the validation rules you have set in your Model? That’s right, the save will fail. But how are you going to tell your user that?

I extended the method by checking the return status of the save operation and throw the validation errors if it failed:

<?php
public function ajax_add() {
    // Save the data passed in the POST request
    $this->InvoiceLine->create();
    if (!$this->InvoiceLine->save($this->request->data)) {
        // Throw a HTTP 400 status
        $this->response->statusCode(400);

        // Set the message to display in our element
        $this->set('message',
            __('The invoice line could not be saved ' .
               'due to these errors:')
        );

        // Loop over all the validationErrors
        $errors = array();
        $valErrors = $this->InvoiceLine->validationErrors;
        foreach ($valErrors as $field) {
            foreach ($field as $rule) {
                array_push($errors, $rule);
            }
        }
        $this->set(compact('errors'));

        // Render the error_dialog element
        $this->render('/Elements/error_dialog');
    }
}

Now, whenever validation fails, the AJAX call will get the validation errors as returned data. Furthermore (and this is the important bit), it throws a HTTP 400 (Bad Request) status, so the AJAX request will fail and it’s error clause is triggered, rather than it’s success clause as it would on a 200 OK status, which would happen if you don’t set the statusCode with the CakeResponse class.

I also created an element called “error_dialog” to hold and display the HTML content to the user. It’s a very simple element that just ouputs our message and an ordered list using the HtmlHelper nestedList method:

<?php
echo $message;
echo $this->Html->nestedList($errors, array(), array(), 'ol');

OK, so now you can use the data from your AJAX call to display to the user somehow. I chose to use jQuery UI Modal Dialog message, so it’s bound to get the users’ attention.

The error dialog

I added the following error clause to my AJAX call:

error: function(jqXHR) {
    $("#NewInvoiceLineErrorDialog").html(
        "<span class=\\"ui-icon ui-icon-alert\\"" +
        "style=\\"float: left; margin: 0 7px 50px 0;\\"></span>" +
        jqXHR.responseText + ""
    );
    $("#NewInvoiceLineErrorDialog").dialog({
        modal: true,
        buttons: {
            "OK": {
                class: "btn btn-primary",
                text: "OK",
                click: function() { $(this).dialog("close"); }
            }
        }
    });
}

This populates the dialog div with the message thrown by our action. In order to properly display this dialog, you need to add a hidden div to your view (I called it NewInvoiceLineErrorDialog), like this:

<div id="NewInvoiceLineErrorDialog" title="<?php echo __('Error'); ?>"
style="display: none;"></div>

Now this div will stay hidden until our error clause is triggered, which will open a popup with the error message. At this point, whenever a user enters bad data, the dialog will pop-up telling them what they have done wrong, so they can fix it and make the save succeed.