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'));
CallNative equivalentDifference
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 equivalentjQuery-only; used by older $.ajax code
$.when(a, b)Promise.alljQuery'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 patternNative replacementNote
$.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 progressOnly relevant for streaming uploads
No equivalentPromise.allSettledUseful 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 async function can await a jQuery promise because it is thenable — but a rejected jQuery promise rejects with a jqXHR object, not an Error, which surprises code that expects error.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.

AJAX helpers and why modern code moves on Form handling, serialization and validation

Last refreshed 2026-09-18.