jQuery AJAX Tutorial: $.ajax, GET, POST, and Error Handling
How I write jQuery AJAX calls that fail gracefully: $.ajax GET and POST, sending JSON, done, fail and always callbacks, and real error output for 404, 500, timeouts and bad JSON on jQuery 1.8.3.

The question usually arrives like this: “I call $.ajax and the request goes out. Then nothing happens, and there is no error.” jQuery AJAX is easy to start and easy to get quietly wrong. Most of its failures stay silent unless you ask for them.
jQuery AJAX means sending an HTTP request from the page with XMLHttpRequest and handling the answer without a reload. jQuery hides the browser differences behind one function. Old Internet Explorer, for example, builds the request object its own way. The official $.ajax documentation covers every option. Here I cover the options I use daily and, more usefully, what each failure looks like.
I ran every example against jQuery 1.8.3, the current release as I write this. A small local test server returns JSON, HTML, errors, empty bodies and slow responses on demand. The output under each snippet is the real output of that run. Once a response arrives, you still need to find the elements to update. My notes on jQuery selectors cover that part.
Why I write the jQuery AJAX failure path first
My rule is to write the failure path first. A request that works on your machine proves very little. The cases that matter happen on a slow mobile connection or behind a proxy. They also happen when the server has just thrown an exception. If you cannot say what the user sees when the request fails, the feature is not finished.
A jQuery AJAX GET request, written the current way
$.ajax({
url: '/json',
type: 'GET',
dataType: 'json',
data: { page: 2, q: 'jquery' } // becomes ?page=2&q=jquery
})
.done(function (data, textStatus, jqXHR) {
console.log('done', textStatus, jqXHR.status, JSON.stringify(data));
})
.fail(function (jqXHR, textStatus, errorThrown) {
console.log('fail', textStatus, errorThrown);
})
.always(function () {
console.log('always');
});
done success 200 {"id":7,"name":"Ana","tags":["a","b"],"q":{"page":"2","q":"jquery"}}
always
Four things happen here. jQuery turns data into an encoded query string (?page=2&q=jquery). dataType: 'json' tells jQuery to parse the response and hand you an object instead of a string. .done() receives that object, the status text and the jqXHR object, which is jQuery’s wrapper around the browser request. Finally, .always() runs whether the request worked or not. I use it to hide a spinner or re-enable a button.
The .done(), .fail() and .always() methods arrived in jQuery 1.5 and 1.6. I prefer them for two reasons. You can attach them after the request starts, and you can attach several to one request. They also make each jQuery AJAX call a promise, which my post on jQuery Deferred and promises covers in depth. The older success, error and complete options still work, as the run below confirms. However, the jqXHR.success() and jqXHR.error() methods are deprecated as of 1.8:
// The older option callbacks still work in jQuery 1.8
$.ajax({
url: '/json',
dataType: 'json',
success: function (data, textStatus) { console.log('success', textStatus); },
error: function () { console.log('error'); },
complete: function (jqXHR, textStatus) { console.log('complete', textStatus); }
});
// But the jqXHR.success() and jqXHR.error() methods are deprecated as of 1.8.
var jqXHR = $.ajax('/json');
console.log(typeof jqXHR.success, typeof jqXHR.error, typeof jqXHR.done, typeof jqXHR.fail);
function function function function
success success
complete success
A POST request, and what goes over the wire
$.ajax({
url: '/echo',
type: 'POST',
data: { name: 'Ana', city: 'Pune' },
dataType: 'json'
}).done(function (reply) {
console.log(reply.method, reply.contentType);
console.log(reply.body);
console.log(reply.accept);
console.log(reply.xrw);
});
POST application/x-www-form-urlencoded; charset=UTF-8
name=Ana&city=Pune
application/json, text/javascript, */*; q=0.01
XMLHttpRequest
The test server echoes what it received, which makes the defaults visible. jQuery sends a plain object in data as a form-encoded body (name=Ana&city=Pune). A PHP script reads it from $_POST['name'] with no extra work. jQuery also adds an Accept header that matches your dataType. It adds X-Requested-With: XMLHttpRequest too. Many server frameworks check that header to tell an AJAX call from a normal page load.
Sending JSON instead of form fields
$.ajax({
url: '/echo',
type: 'POST',
contentType: 'application/json; charset=utf-8',
data: JSON.stringify({ name: 'Ana' }), // you must stringify yourself
dataType: 'json'
}).done(function (reply) {
console.log(reply.contentType);
console.log(reply.body);
});
application/json; charset=utf-8
{"name":"Ana"}
To send JSON, you do two things yourself. Set contentType, and turn the object into a string with JSON.stringify. If you pass an object to data without stringifying it, jQuery form-encodes it anyway. Two practical warnings follow. First, PHP does not fill $_POST for a JSON body, so read php://input and decode it. Second, Internet Explorer 7 has no built-in JSON object. Include Douglas Crockford’s json2.js if you support it.
jQuery AJAX error handling: five ways a request can fail
function tryUrl(options) {
$.ajax(options)
.done(function (data, textStatus, jqXHR) {
console.log(options.url, 'DONE', textStatus, jqXHR.status, JSON.stringify(data));
})
.fail(function (jqXHR, textStatus, errorThrown) {
console.log(
options.url, 'FAIL',
'status=' + jqXHR.status,
'textStatus=' + textStatus,
'errorThrown=' + (errorThrown && errorThrown.name ? errorThrown.name : errorThrown)
);
});
}
tryUrl({ url: '/missing', dataType: 'json' });
tryUrl({ url: '/fail', dataType: 'json' });
tryUrl({ url: '/bad-json', dataType: 'json' });
tryUrl({ url: '/html-as-json', dataType: 'json' });
tryUrl({ url: '/slow?ms=1500', dataType: 'json', timeout: 300 });
tryUrl({ url: '/empty', dataType: 'json' });
/missing FAIL status=404 textStatus=error errorThrown=Not Found
/fail FAIL status=500 textStatus=error errorThrown=Internal Server Error
/bad-json FAIL status=200 textStatus=parsererror errorThrown=SyntaxError
/html-as-json FAIL status=200 textStatus=parsererror errorThrown=SyntaxError
/empty DONE success 204 null
/slow?ms=1500 FAIL status=0 textStatus=timeout errorThrown=timeout
Read this jQuery AJAX output line by line. It is the most useful table on this page:
| Case | HTTP status | textStatus | What it means |
|---|---|---|---|
/missing |
404 | error |
Server answered with an error status; errorThrown is the reason phrase |
/fail |
500 | error |
Server-side exception |
/bad-json |
200 | parsererror |
The request succeeded but the body is not valid JSON |
/html-as-json |
200 | parsererror |
Typically a login page or error page sent where JSON was expected |
/slow |
0 | timeout |
No answer within the timeout option; status 0 means “no response at all” |
/empty |
204 | success |
Success with no body, so the data argument is undefined or null |
People miss the two parsererror rows. The HTTP status is 200, so the server thinks it succeeded. But jQuery asked for JSON and could not parse the body, so your .fail() handler runs. A failure handler that assumes 4xx or 5xx shows the wrong message here. Branch on textStatus first and the status code second. Calling jqXHR.abort() produces a fourth value, abort. I usually ignore it instead of showing it to the user.
Status 0 covers several causes. The browser may have blocked the request under the same-origin policy. The server may be down. Or the user navigated away mid-request. JavaScript cannot see which one happened. So keep the status 0 message honest and generic: “Could not reach the server. Check your connection and try again.”
Shortcuts, caching and global defaults
// cache: false appends a timestamp so an old copy is never reused
$.ajax({ url: '/count', cache: false, dataType: 'json' }).done(function (d) {
console.log('cache:false ->', d.query.replace(/_=\d+/, '_=<timestamp>'));
});
// Shortcuts are $.ajax with some options filled in
$.getJSON('/json?x=1').done(function (d) { console.log('$.getJSON ->', JSON.stringify(d.q)); });
$.post('/echo', { a: 1 }, function (d) { console.log('$.post ->', d.method, d.body); }, 'json');
cache:false -> ?_=<timestamp>
$.getJSON -> {"x":"1"}
$.post -> POST a=1
$.get, $.post and $.getJSON are $.ajax with the method and data type filled in. I use them when I only need the happy path, and I chain .fail() onto them for the rest. The cache: false option appends a unique _=timestamp parameter to GET requests. It looks like a hack, yet you need it. Internet Explorer caches GET responses hard. Without it, a “refresh the list” button can keep showing the old list.
$.ajaxSetup({ timeout: 5000, cache: false });
console.log($.ajaxSettings.timeout, $.ajaxSettings.cache);
5000 false
$.ajaxSetup() changes the defaults for every later request. That suits a timeout or a header. But it also affects third-party plugins on the page. Keep it to a few options you fully understand.
Global events for one place to handle failure
var events = [];
$(document)
.ajaxStart(function () { events.push('start'); })
.ajaxError(function (e, jqXHR) { events.push('error:' + jqXHR.status); })
.ajaxStop(function () { events.push('stop'); console.log(events.join(' > ')); });
$.ajax('/json');
$.ajax('/missing');
start > error:404 > stop
The ajaxStart, ajaxError and ajaxStop events fire for every jQuery AJAX request on the page. I use them for a single “Loading” indicator. They also let me log failures in one place instead of in forty .fail() handlers. Note that ajaxStart and ajaxStop fired once for two simultaneous requests. They track “any request in flight”, not each request.
Troubleshooting jQuery AJAX calls
| Symptom | Cause | Fix |
|---|---|---|
| Nothing happens, no error shown | No .fail() handler, or the code that uses the data runs before the response arrives |
Put dependent code inside .done(); AJAX is asynchronous |
| Fails with status 0 on a local file | Browsers block XMLHttpRequest from file:// pages |
Test through a local web server |
| Status 0 calling another domain | Same-origin policy | Use JSONP (dataType: 'jsonp') if the API offers it, or a server-side proxy, or CORS where the server and browser support it |
parsererror on a 200 |
Body is not valid JSON | Check the raw jqXHR.responseText; fix the server or the dataType |
| Old data in Internet Explorer | GET response cached | cache: false |
$_POST is empty in PHP |
Body sent as JSON | Read php://input, or send form fields |
| Page freezes during the request | async: false |
Remove it and use callbacks |
How I build AJAX features on real sites
- The server uses honest status codes. A 200 that contains
{"error": "..."}hides failures from.fail(). Return a 4xx or 5xx with a JSON body that explains the problem. - One wrapper per project. I wrap every jQuery AJAX call in a small function. It sets the timeout, adds the CSRF token header, logs failures and shows the generic error. No single feature can forget any of that.
- The UI shows the in-flight state. I disable the submit button when the call starts and re-enable it in
.always(). A double-click then cannot create two records. - I treat response text as untrusted. To insert server text, use
.text()rather than.html()unless the HTML is yours. Otherwise a stored comment with a script tag becomes a cross-site scripting hole.
The silent failure I meet most often in jQuery AJAX code looks harmless. A login session expires, the server redirects the request to the login page, and the browser follows the redirect. The call ends with a 200 and an HTML page where JSON should be. jQuery reports parsererror, and a .fail() handler that only checks for 4xx or 5xx shows nothing useful. I now treat parsererror as “your session may have expired” and offer a reload. That one branch has saved me hours of guessing.
Questions I answer before writing a request
- GET or POST? Reading data that is safe to repeat is a GET. Anything that changes server state is a POST.
- What comes back? Set
dataTypeexplicitly rather than letting jQuery guess from the response headers. - What are the failure cases? Decide the message for status 0, for 4xx and 5xx, for
parsererror, and for timeout before writing the success path. - What does the user see while waiting? Show progress and prevent a second submit.
- Is it cross-domain? If yes, decide between JSONP, CORS and a server proxy before writing any jQuery.
When not to use AJAX
- A simple form that works as a normal POST. Build the plain form first. Add AJAX as an enhancement, so the page still works without JavaScript.
- Uploading files. Internet Explorer 9 and earlier lack the XMLHttpRequest features for file upload with progress. Use a plugin that falls back to a hidden iframe.
- Content search engines need to see. Search engines may not index content that loads after the page renders. Put important text in the initial HTML. For content that can wait, I show how to load data without a page refresh in a separate post.
Common jQuery AJAX mistakes
- Using the response outside the callback. The line after
$.ajax()runs before the response exists. - Handling only HTTP errors. A
parsererroror timeout has different data and needs a different message. - Setting
contentTypeto JSON without stringifying. jQuery still sends form fields. - Setting
async: false. It freezes the browser, and jQuery 1.8 deprecated it for the callback style. - Building query strings by hand. Pass an object to
dataand let jQuery encode the values. - Inserting responses with
.html(). Use.text()for anything a user could have written.
FAQ
How do I make an AJAX call with jQuery?
Call $.ajax() with a url, a type such as GET, a dataType such as json, and any data. Then attach .done(function (data) { }) for success. Attach .fail(function (jqXHR, textStatus) { }) for errors. The shortcuts $.get, $.post and $.getJSON do the same with fewer options.
What is the difference between $.get, $.post and $.ajax?
$.get and $.post are shorthand for $.ajax with the HTTP method preset and fewer options. Use them for simple requests. Use $.ajax directly when you need a timeout, custom headers, a JSON request body or control over caching.
How do I handle errors in jQuery AJAX?
Attach .fail() and check textStatus. You get error for an HTTP error status and parsererror when jQuery cannot parse the body. You get timeout when the timeout option expires, and abort when you cancel the request. Show a generic message for status 0.
Why does my jQuery AJAX request return parsererror?
You asked jQuery for JSON or XML, and the response body was not valid. The HTTP status can still be 200. The usual cause is a server that returns an HTML error page or a login page. Inspect jqXHR.responseText to see what came back.
How do I send JSON in a jQuery AJAX POST?
Set contentType: 'application/json; charset=utf-8' and pass data: JSON.stringify(obj). Without stringifying, jQuery sends form-encoded fields. Older Internet Explorer needs json2.js for JSON.stringify, and PHP reads the body from php://input.
jQuery AJAX is easy to start and hard to finish. The real work sits in the cases that never happen on your own machine. Think slow networks, bad responses and double clicks. So I write the .fail() handler before the .done() handler. And I branch on textStatus before I look at the HTTP status.
Originally posted on 27 November 2012 and post last modified on 2 November 2026