Deferreds, promises and async patterns
Use $.Deferred and $.when for real sequencing and parallel work, understand then versus done, and map the same flows onto native promises.
$.Deferred and the jQuery promise
// A Deferred is a promise you control: you create it and you resolve it.
function wait(ms) {
const deferred = $.Deferred();
setTimeout(() => deferred.resolve('done after ' + ms), ms);
return deferred.promise(); // hand out the read-only half
}
wait(500)
.done((message) => console.log(message))
.fail((error) => console.error(error))
.always(() => console.log('cleanup'));
// A deferred can also be rejected, and settled twice only once.
const d = $.Deferred();
d.reject('nope');
d.resolve('too late'); // ignored — a deferred settles exactly once
console.log(d.state()); // 'rejected'
// Wait for several deferreds
$.when(wait(200), wait(400)).done(() => console.log('both finished'));| Call | Native equivalent | Difference |
|---|---|---|
deferred.resolve(v) | resolve(v) | jQuery passes multiple arguments |
deferred.reject(e) | reject(e) | Same idea |
.done(fn) | .then(fn) | done returns the same promise and does not transform |
.fail(fn) | .catch(fn) | Same |
.always(fn) | .finally(fn) | Same |
.progress(fn) | No equivalent | jQuery-only; used by older $.ajax code |
$.when(a, b) | Promise.all | jQuery's version behaves differently with multiple arguments |
then, chaining and the classic traps
// .then() in jQuery 3 is Promises/A+ compliant: returning a value transforms
// the chain, returning a promise waits for it, throwing rejects it.
loadConfig()
.then((config) => loadUser(config.userId)) // returns a promise: chain waits
.then((user) => user.name)
.then((name) => console.log('Hello', name))
.catch((error) => console.error('failed:', error));
// .done() does NOT transform and does not catch downstream errors.
// Mixing done() into a chain is the most common Deferred bug:
loadConfig()
.done((config) => { throw new Error('swallowed'); }) // unhandled, not caught below
.fail((error) => console.error('never runs'));
// Error handling: catch is an alias of fail in jQuery, unlike native promises
// where catch and then(null, fn) are equivalent. Chain position matters.
$.ajax('/api/broken')
.then((data) => console.log(data))
.catch((xhr) => console.error('status', xhr.status)); // receives the jqXHR// Parallel work with a genuine error path
function fetchAll(ids) {
const requests = ids.map((id) => $.ajax({ url: '/api/items/' + id, method: 'GET' }));
return $.when.apply($, requests).then(function () {
// $.when passes each result as a separate argument
const results = Array.prototype.slice.call(arguments).map((args) => args[0]);
return results;
});
}
fetchAll(['a', 'b', 'c'])
.then((items) => render(items))
.catch(() => showError('Could not load items'));
// The modern version of the same function, no jQuery required:
async function fetchAllNative(ids) {
const responses = await Promise.all(ids.map((id) => fetch('/api/items/' + id)));
if (responses.some((r) => !r.ok)) throw new Error('One or more requests failed');
return Promise.all(responses.map((r) => r.json()));
}⚠️
$.when with a non-promise argument resolves immediately and ignores later failures — and with an empty array it resolves with no arguments. Both behaviours differ from Promise.all, which rejects on the first failure and resolves an empty array cleanly. Wrap it carefully or move the code to native promises.Practical patterns and the migration path
// Pattern 1: a function that returns a promise instead of taking callbacks.
function loadUser(id) {
return $.ajax({ url: '/api/users/' + id, method: 'GET', dataType: 'json' });
}
// callers can chain, await, or use $.when — the caller decides
// Pattern 2: cache in-flight requests so a double click does not double-fetch
const inFlight = new Map();
function loadOnce(id) {
if (!inFlight.has(id)) {
const request = loadUser(id).always(() => inFlight.delete(id));
inFlight.set(id, request);
}
return inFlight.get(id);
}
// Pattern 3: sequence a queue of dependent operations
function sequence(tasks) {
return tasks.reduce(
(chain, task) => chain.then((previous) => task(previous)),
$.Deferred().resolve().promise()
);
}
// Pattern 4: convert to a native promise at the boundary. jQuery's thenable is
// compatible enough that most tools accept it, but awaiting it is clearer.
function loadUserNative(id) {
return fetch('/api/users/' + id).then((res) => {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.json();
});
}| jQuery pattern | Native replacement | Note |
|---|---|---|
$.ajax().done() | await fetch().then() | No automatic JSON parsing — call .json() |
$.when(a, b) | Promise.all([a, b]) | Rejects on the first failure |
$.Deferred() | new Promise((resolve, reject) => {}) | No .progress() equivalent |
.always() | .finally() | Same semantics |
.progress() | ReadableStream progress | Only relevant for streaming uploads |
| No equivalent | Promise.allSettled | Useful when partial success is acceptable |
- Returning a promise from a function is the single change that makes a callback-heavy module testable and composable. It costs nothing and can be done one function at a time.
- Boundary conversion is a good migration strategy: keep jQuery internally, but expose native promises from the module so new code never has to know.
- An
asyncfunction canawaita jQuery promise because it is thenable — but a rejected jQuery promise rejects with ajqXHRobject, not anError, which surprises code that expectserror.message.
FAQ
Is a jQuery promise a real Promise?
No, but it is thenable, which is what
await and most interop libraries need. It lacks Promise.prototype.catch semantics in some positions and rejects with a jqXHR rather than an Error, so wrap it if downstream code expects the standard shape.Why is my .fail() not running?
Most often the chain used
.done() before it: done does not propagate a rejection the way then does. Use .then().catch() for real chains and keep .done() for fire-and-forget handlers.Related
AJAX helpers and why modern code moves on Form handling, serialization and validation
Last refreshed 2026-09-18.