33

If I navigate away from a page in the middle of an $.ajax() request it fires the error callback. I've tested in Safari and FF with both GET and POST requests.

One potential solution would be to abort all AJAX requests on page unload, but the error handler is called before unload, so this doesn't seem possible.

I want to be able to handle REAL errors such as 500s gracefully on the client side with a polite alert or a modal dialog, but I don't want this handling to be called when a user navigates away from the page.

How do I do this?

--

(Also strange: When navigating away from a page, the error handler says that the textStatus parameter is "error", the same it throws when receiving a 500/bad request.)

Chris
  • 5,876
  • 3
  • 43
  • 69
Graham
  • 353
  • 3
  • 8

6 Answers6

24

In the error callback or $.ajax you have three input arguments:

function (XMLHttpRequest, textStatus, errorThrown) {
   this; // options for this ajax request
}

You can check directly the xhr.status to get the HTTP response code, for example:

$.ajax({
  url: "test.html",
  cache: false,
  success: function(html){
    $("#results").append(html);
  },
  error: function (xhr, textStatus) {
    if (xhr.status == 500) {
      alert('Server error: '+ textStatus);
    }
  }
});

Edit: To tell the difference between a connection broken by the browser and the case where the server is down (jasonmerino's comment):

On unload the xhr.readyState should be 0, where for a non responsive server the xhr.readyState should be 4.

Cebjyre
  • 6,552
  • 3
  • 32
  • 57
Christian C. Salvadó
  • 807,428
  • 183
  • 922
  • 838
  • 3
    Status = 0 also if server is not responding (is down). – Mitar Nov 07 '11 at 03:39
  • 2
    Since it has the same status value, how to differentiate unload and not responding? – Jeaf Gilbert Nov 18 '11 at 16:04
  • 5
    On unload the `xhr.readyState` should be 0, where for a non responsive server the `xhr.readyState` should be 4. – jasonmerino Feb 16 '12 at 16:18
  • ff 16 + msft iis. stopping server in the midlle of response also return readyState==0. seem to me "before unload" + flag and then checking it in error handler is more reliable – Yauhen.F Mar 21 '13 at 11:49
  • 1
    Does it make sense then that I get a `status: 404` with `readyState: 4`, when the user browses away during a long running ajax request? (FF 23). – mediafreakch Aug 27 '13 at 13:00
  • Why don't you just use `textStatus` instead of `xhr.status` to check the `abort` status? – Alvaro Nov 10 '15 at 14:52
13

This is a tough one to handle correctly in all situations. Unfortunately in many popular browsers the xhr.status is the same (0) if the AJAX call is cancelled by navigation or by a server being down / unresponsive. So that technique rarely works.

Here's a set of highly "practical" hacks that I've accumulated that work fairly well in the majority of circumstances, but still isn't bullet-proof. The idea is to try to catch the navigate events and set a flag which is checked in the AJAX error handler. Like this:

var global_is_navigating = false;

$(window).on('beforeunload',function() {
    // Note: this event doesn't fire in mobile safari
    global_is_navigating = true;
});

$("a").on('click',function() {
    // Giant hack that can be helpful with mobile safari
    if( $(this).attr('href') ) {
        global_is_navigating = true;
    }
});

$(document).ajaxError(function(evt, xhr, settings) {
    // default AJAX error handler for page
    if( global_is_navigating ) {
        // AJAX call cancelled by navigation. Not a real error
        return;
    }
    // process actual AJAX error here.
});
Leopd
  • 41,333
  • 31
  • 129
  • 167
  • 1
    Thanks for the solution, just noticed a typo in the code. At the end of code a parenthesis and semi colon is missing. So the last method will look like this. `$(document).ajaxError(function(evt, xhr, settings) { // default AJAX error handler for page if( global_is_navigating ) { // AJAX call cancelled by navigation. Not a real error return; } });` – Muein Muzamil Aug 26 '13 at 07:13
  • FYI, the workaround for the mobile version of Safari is still needed for iOS 10 – Adam Taylor Dec 22 '16 at 14:18
  • Important - malformed anchor links sometimes trigger false unload events. To protect against it, I add setTimeout(function(){global_is_navigating = false;}, 1000) in the handler - if a second has passed and we are still alive and could execute the timer, then it was false unload and next errors should not be ignored. Issue - of course, there can be an actual AJAX error during that 1 second and we could miss that... – JustAMartin Dec 08 '17 at 12:19
  • The navigation can be cancelled in `beforeunload`. iOS Safari doesn't fire `beforeunload`. This code example doesn't consider these features. – Finesse Dec 03 '20 at 11:04
4

(I'd add this as a comment to the main answer but haven't built up enough points to do so yet!)

I'm also seeing this in FF4 and Chrome (9.0.597.107). Probably elsewhere but that's bad enough for me to want to fix it!

One of the things that's odd about this situation is that returned XMLHttpRequest.status === 0

Which seems like a reliable way to detect this situation and, in my particular case, abort the custom error handling that displays to the user:

error: function (XMLHttpRequest, textStatus, errorThrown) {
    if (XMLHttpRequest.status === 0) return;
    // error handling here
}

Also worth mentioning that on the assumption if may be a problem in the JSON parse of whatever the browser is giving back to the $.ajax() call, I also tried swapping out the native JSON.stringify for the Douglas Crockford version ( https://github.com/douglascrockford/JSON-js ) but that made no difference.

RedYeti
  • 1,024
  • 14
  • 28
  • 4
    Status = 0 also if server is not responding (is down). – Mitar Nov 07 '11 at 03:40
  • I've just experimented and yes - the 0 is returned if the HTTP server (Apache in my case) is either already down when the request is made, or is terminated whilst the request is with the app server (JBoss in my case). – RedYeti Feb 06 '12 at 11:58
  • For what it's worth: Terminating the app server mid-request (I held it on a breakpoint) returned me a 502 status whereas if the app server wasn't running when I made the request I got a 503 (HTTP server was running both times of course). – RedYeti Feb 06 '12 at 12:00
0

The error callback should get a reference to the XHR object, check the status code to see if it's a server error or not?

great_llama
  • 11,481
  • 4
  • 34
  • 29
0

If you make use of jQuery functions such as $.get, $.post, $.ajax... then you can use the parameter text_status to check what type of fail is:

request = $.get('test.html', function(data){
    //whatever
}).fail(function(xhr, text_status, error_thrown) {

    if(text_status!== 'abort'){
        console.warn("Error!!");
    }
});

From the jQuery docs:

jqXHR.fail(function( jqXHR, textStatus, errorThrown ) {}); An alternative construct to the error callback option, the .fail() method replaces the deprecated .error() method. Refer to deferred.fail() for implementation details.

Alvaro
  • 40,778
  • 30
  • 164
  • 336
0

Just wait a while before showing the error. If a request fails due to page unload, the timeout won't fire and the error won't be shown. My experiments have shown that 100ms is enough for desktop Safari and Firefox (desktop and Android) and 500ms is enough for iOS Safari. The durations are fuzzy, so you should increase the numbers if it's possible.

const request = new XMLHttpRequest();

// Firefox calls onabort, Safari calls onerror
request.onabort = request.onerror = () => {
  // A short comprehensive solution
  setTimeout(() => {
    alert('The request has failed completely');
  }, 500);

  // A more gentle solution
  const isWebKit = 'ApplePayError' in window;
  const isDesktopSafari = !('standalone' in navigator);
  const isGecko = 'mozInnerScreenX' in window
  setTimeout(
    () => {
      alert('The request has failed completely');
    },
    isWebKit ? (isDesktopSafari ? 100 : 500) : (isGecko ? 100 : 0) 
  );
}

request.open('get', 'http://example.com', true);
request.send();

I've checked this solutions only in modern browsers (Chrome 87, Firefox 83, IE 11, Edge, Safari 14).

Finesse
  • 9,793
  • 7
  • 62
  • 92