Dojo's promise system — Deferred, chaining, parallel ops, request/xhr, and production error handling patterns
dojo/Deferred — Resolve, Reject, Progress.then() Chaining.otherwise() — Error Handlingdojo/when — Values & Promises Unifieddojo/promise/all — Parallel Operationsdojo/request/xhr — HTTP Requestsdojo/request — Unified APIdojo/request/notify — Global HooksDeferred predates and predicts native Promises, how to chain async server calls without nesting, how to run multiple requests in parallel, and how to build a robust retry mechanism for flaky APIs.
Before Dojo's Deferred (and native Promises), multi-step async workflows nested callbacks — each error case had to be handled separately, and the happy path was buried under indentation.
// Callback hell — loading a user, their orders, then a product
// Notice: error handlers duplicated at every level
loadUser(userId, function(err, user) {
if (err) { showError(err); return; }
loadOrders(user.id, function(err, orders) {
if (err) { showError(err); return; }
loadProduct(orders[0].productId, function(err, product) {
if (err) { showError(err); return; }
loadReviews(product.id, function(err, reviews) {
if (err) { showError(err); return; }
// Finally: actual work — buried 4 levels deep
renderDashboard(user, orders, product, reviews);
});
});
});
});
Dojo's Deferred (introduced 2011) solves this with three key ideas:
// The same flow with Deferred — flat, readable, one error handler
loadUser(userId)
.then(function(user) { return loadOrders(user.id); })
.then(function(orders) { return loadProduct(orders[0].productId); })
.then(function(product) { return loadReviews(product.id); })
.then(function(reviews) { renderDashboard(reviews); })
.otherwise(showError); // one handler for all failures
dojo/Deferred — Resolve, Reject, ProgressA Deferred is an object representing a value that will be available in the future. It has two sides: the resolver (producer) and the promise (consumer). You resolve or reject via the Deferred; you chain via its promise.
require(["dojo/Deferred"], function(Deferred) {
// ── Creating a Deferred ───────────────────────────────────────
function delayedMultiply(a, b, delayMs) {
var deferred = new Deferred();
setTimeout(function() {
if (typeof a !== "number" || typeof b !== "number") {
deferred.reject(new Error("Both arguments must be numbers"));
} else {
deferred.resolve(a * b); // success — value flows to .then()
}
}, delayMs);
return deferred.promise; // return the PROMISE, not the Deferred
// Callers get the promise (read-only) — they cannot resolve/reject it
}
// ── Consuming the promise ─────────────────────────────────────
delayedMultiply(6, 7, 1000)
.then(function(result) {
console.log("Result:", result); // 42 — fires after 1 second
})
.otherwise(function(err) {
console.error("Error:", err.message);
});
// ── Progress notifications ─────────────────────────────────────
function longRunningTask() {
var deferred = new Deferred();
var step = 0;
var interval = setInterval(function() {
step++;
deferred.progress(step * 10); // notify: 10%, 20%, ...
if (step >= 10) {
clearInterval(interval);
deferred.resolve("Complete");
}
}, 200);
return deferred.promise;
}
longRunningTask()
.then(
function(result) { console.log("Done:", result); }, // onFulfilled
function(err) { console.error("Failed:", err); }, // onRejected
function(progress) { updateProgressBar(progress); } // onProgress
);
// ── Deferred state inspection ─────────────────────────────────
var d = new Deferred();
console.log(d.isFulfilled()); // false
console.log(d.isRejected()); // false
console.log(d.isCanceled()); // false
d.resolve(42);
console.log(d.isFulfilled()); // true
console.log(d.promise.isResolved()); // true
});
| dojo/Deferred (1.17) | Native Promise (ES6) | |
|---|---|---|
| Progress | Yes — deferred.progress(v) | No built-in progress |
| Cancellation | Yes — deferred.cancel() | No (AbortController is separate) |
| Sync resolve | Yes — resolve() before .then() | Always async (microtask) |
| Error keyword | .otherwise(fn) | .catch(fn) |
| Interop | dojo/when wraps native Promises | Needs wrapper for Dojo chains |
.then() ChainingEach .then() returns a new promise. Whatever you return from a .then() callback becomes the resolved value for the next .then(). Return another promise and the chain waits for it.
require(["dojo/request/xhr"], function(xhr) {
// ── Sequential chain — each step waits for the previous ──────
xhr.get("/api/session", { handleAs: "json" })
.then(function(session) {
// Return a new XHR — chain waits for it
return xhr.get("/api/users/" + session.userId, { handleAs: "json" });
})
.then(function(user) {
return xhr.get("/api/teams/" + user.teamId, { handleAs: "json" });
})
.then(function(team) {
renderHeader(team);
return team; // pass team to next step
})
.then(function(team) {
return xhr.get("/api/teams/" + team.id + "/members", { handleAs: "json" });
})
.then(function(members) {
renderMemberList(members);
})
.otherwise(function(err) {
// Catches errors from ANY step in the chain
showErrorBanner("Failed to load: " + err.message);
});
// ── Transforming values — no async op needed ──────────────────
xhr.get("/api/employees", { handleAs: "json" })
.then(function(data) {
// Synchronous transform — return value (not a promise)
return data.filter(function(e) { return e.active; })
.sort(function(a, b) { return a.name.localeCompare(b.name); });
})
.then(function(activeEmployees) {
renderList(activeEmployees); // receives the filtered+sorted array
});
// ── Branching — different paths based on result ───────────────
xhr.get("/api/user/role", { handleAs: "json" })
.then(function(data) {
if (data.role === "admin") {
return xhr.get("/api/admin/dashboard", { handleAs: "json" });
} else {
return xhr.get("/api/user/dashboard", { handleAs: "json" });
}
})
.then(function(dashboardData) {
renderDashboard(dashboardData);
});
});
return inside a .then(), the next handler receives undefined instead of the value. The chain continues but silently loses data.
// WRONG — next .then() receives undefined
.then(function(user) {
xhr.get("/api/orders/" + user.id, { handleAs: "json" }); // no return!
})
.then(function(orders) {
console.log(orders); // undefined
})
// CORRECT
.then(function(user) {
return xhr.get("/api/orders/" + user.id, { handleAs: "json" });
})
.otherwise() — Error Handlingrequire(["dojo/request/xhr"], function(xhr) {
// ── Basic error catch ──────────────────────────────────────────
xhr.get("/api/data", { handleAs: "json" })
.then(function(data) { renderData(data); })
.otherwise(function(err) {
// err is an Error object for network failures
// err.response is the XHR response for HTTP errors
console.error(err.message);
if (err.response) {
console.error("Status:", err.response.status);
console.error("Body:", err.response.text);
}
});
// ── Recovery — handle error, continue chain ────────────────────
xhr.get("/api/prefs", { handleAs: "json" })
.otherwise(function(err) {
// Return a fallback value — chain continues normally
console.warn("Could not load prefs, using defaults:", err.message);
return { theme: "claro", pageSize: 25 }; // fallback object
})
.then(function(prefs) {
// prefs is either the loaded value OR the fallback — code doesn't care
applyPreferences(prefs);
});
// ── Re-throwing — escalate after partial handling ─────────────
xhr.get("/api/data", { handleAs: "json" })
.otherwise(function(err) {
logError(err); // log it
throw err; // re-throw to keep chain in error state
})
.otherwise(function(err) {
showUserFacingError(); // user-facing message
});
// ── Per-step error handling vs single handler ─────────────────
// Single handler (recommended — simpler)
step1().then(step2).then(step3).otherwise(handleAnyError);
// Per-step (use when steps have different recovery strategies)
step1()
.then(step2, function(err) { return fallback2(); }) // recover step2
.then(step3)
.otherwise(handleFatalError);
});
dojo/when — Values & Promises Unifieddojo/when normalizes both plain values and promises into the same handling pattern. Pass it either a value or a promise — it always calls your callback the same way. This is essential when writing functions that might receive either synchronous cache hits or async fetch results.
require(["dojo/when", "dojo/request/xhr"], function(when, xhr) {
// ── Basic usage ────────────────────────────────────────────────
// With a plain value — callback fires synchronously
when(42, function(val) {
console.log("Got:", val); // 42 — runs immediately
});
// With a promise — callback fires when resolved
var p = xhr.get("/api/user", { handleAs: "json" });
when(p, function(user) {
console.log("Got:", user); // fires when XHR completes
});
// ── The real power: cache-or-fetch pattern ─────────────────────
var cache = {};
function getEmployee(id) {
if (cache[id]) {
return cache[id]; // plain value (synchronous cache hit)
}
return xhr.get("/api/employees/" + id, { handleAs: "json" })
.then(function(emp) {
cache[id] = emp; // store in cache
return emp;
});
// Returns either a plain value OR a promise
}
// when() handles both cases identically — caller doesn't need to know
when(getEmployee(42), function(emp) {
renderCard(emp);
}, function(err) {
showError(err);
});
// ── Using when() in a widget ────────────────────────────────────
var MyWidget = declare([_Widget], {
dataSource: null, // can be set to a value OR a Deferred/promise
startup: function() {
this.inherited(arguments);
var self = this;
// Works whether dataSource is already data, a Deferred, or null
when(this.dataSource, function(data) {
self._render(data || []);
});
}
});
// Usage 1: immediate data
new MyWidget({ dataSource: employees }).placeAt("app").startup();
// Usage 2: async fetch — widget handles both transparently
new MyWidget({
dataSource: xhr.get("/api/employees", { handleAs: "json" })
}).placeAt("app").startup();
});
dojo/promise/all — Parallel Operationsdojo/promise/all fires multiple async operations simultaneously and resolves when ALL of them complete. It's the equivalent of Promise.all() in modern JS.
require([
"dojo/promise/all",
"dojo/request/xhr"
], function(all, xhr) {
// ── Array form — results at same indexes ──────────────────────
all([
xhr.get("/api/employees", { handleAs: "json" }),
xhr.get("/api/departments", { handleAs: "json" }),
xhr.get("/api/locations", { handleAs: "json" })
]).then(function(results) {
var employees = results[0]; // from first XHR
var departments = results[1];
var locations = results[2];
renderDashboard(employees, departments, locations);
}).otherwise(function(err) {
// Fires if ANY request fails — partial results are discarded
showError("Failed to load page data: " + err.message);
});
// ── Object form — named results (cleaner for many requests) ───
all({
user: xhr.get("/api/me", { handleAs: "json" }),
prefs: xhr.get("/api/me/preferences", { handleAs: "json" }),
notices: xhr.get("/api/me/notifications",{ handleAs: "json" }),
team: xhr.get("/api/me/team", { handleAs: "json" })
}).then(function(results) {
// Keys match the keys in the input object
renderHeader(results.user);
applyPrefs(results.prefs);
renderNotifications(results.notices);
renderTeamWidget(results.team);
});
// ── Mixed: values + promises ───────────────────────────────────
var cachedConfig = { apiBase: "/api/v2", pageSize: 25 };
all({
config: cachedConfig, // plain value
employees: xhr.get("/api/employees", { handleAs: "json" }) // promise
}).then(function(results) {
// works: dojo/when under the hood normalizes the plain value
initApp(results.config, results.employees);
});
// ── Sequential with parallel sub-steps ────────────────────────
// Load user, THEN in parallel load their orders AND preferences
xhr.get("/api/me", { handleAs: "json" })
.then(function(user) {
return all({
orders: xhr.get("/api/orders?userId=" + user.id, { handleAs: "json" }),
prefs: xhr.get("/api/prefs?userId=" + user.id, { handleAs: "json" })
});
})
.then(function(results) {
renderOrders(results.orders);
applyPrefs(results.prefs);
});
});
promise/all to fire them simultaneously. Sequential chaining is only needed when step N requires the result of step N-1. Parallel requests can cut load time dramatically on page-load scenarios.
dojo/request/xhr — HTTP Requestsrequire(["dojo/request/xhr"], function(xhr) {
// ── GET ────────────────────────────────────────────────────────
xhr.get("/api/employees", {
handleAs: "json", // auto parse response body
headers: { "X-Auth": token },
timeout: 5000 // ms before request fails
}).then(function(employees) {
renderList(employees);
}).otherwise(function(err) {
console.error(err.response.status, err.message);
});
// ── POST (JSON body) ───────────────────────────────────────────
xhr.post("/api/employees", {
handleAs: "json",
headers: { "Content-Type": "application/json" },
data: JSON.stringify({ name: "Alice", dept: "Engineering" })
}).then(function(created) {
console.log("Created with id:", created.id);
});
// ── PUT (update) ───────────────────────────────────────────────
xhr({
url: "/api/employees/42",
method: "PUT",
handleAs: "json",
headers: { "Content-Type": "application/json" },
data: JSON.stringify(updatedEmployee)
}).then(function(updated) {
store.put(updated); // sync the Dojo store
});
// ── DELETE ─────────────────────────────────────────────────────
xhr({
url: "/api/employees/42",
method: "DELETE"
}).then(function() {
store.remove(42);
});
// ── Form data (multipart) ──────────────────────────────────────
var formData = new FormData();
formData.append("file", fileInput.files[0]);
formData.append("userId", "42");
xhr.post("/api/upload", {
data: formData,
// Do NOT set Content-Type here — browser sets multipart boundary automatically
handleAs: "json"
}).then(function(result) {
console.log("Uploaded:", result.url);
});
// ── Query string parameters ────────────────────────────────────
xhr.get("/api/employees", {
handleAs: "json",
query: { // automatically serialized: ?dept=Engineering&active=true
dept: "Engineering",
active: true,
page: 1,
size: 25
}
});
// ── Response object ────────────────────────────────────────────
// Access raw response: .response property on the promise
var req = xhr.get("/api/data", { handleAs: "json" });
req.then(function(data) {
console.log("Data:", data); // parsed body
});
req.response.then(function(response) {
console.log("Status:", response.status); // 200
console.log("Headers:", response.getHeader("X-Total-Count"));
console.log("Raw text:", response.text); // unparsed body
});
});
dojo/request — Unified APIdojo/request is a platform-aware wrapper that selects the right transport automatically: XHR in browsers, node/request in Node.js. Use it instead of dojo/request/xhr directly when your code might run in multiple environments.
require(["dojo/request"], function(request) {
// Same API as dojo/request/xhr — platform is auto-selected
request("/api/employees", {
method: "GET",
handleAs: "json"
}).then(function(data) {
renderList(data);
});
// Shorthand methods
request.get("/api/employees", { handleAs: "json" }).then(renderList);
request.post("/api/employees", { data: JSON.stringify(emp), handleAs: "json" });
request.put("/api/employees/1", { data: JSON.stringify(emp), handleAs: "json" });
request.del("/api/employees/1"); // .del() not .delete() — reserved keyword
// ── Constructing a REST service wrapper ───────────────────────
// Clean pattern for talking to a REST API from multiple widgets
define(["dojo/request", "dojo/_base/lang"], function(request, lang) {
var BASE = "/api/v2";
return {
getAll: function(resource, params) {
return request.get(BASE + "/" + resource, {
handleAs: "json",
query: params || {}
});
},
getOne: function(resource, id) {
return request.get(BASE + "/" + resource + "/" + id, {
handleAs: "json"
});
},
create: function(resource, data) {
return request.post(BASE + "/" + resource, {
handleAs: "json",
headers: { "Content-Type": "application/json" },
data: JSON.stringify(data)
});
},
update: function(resource, id, data) {
return request.put(BASE + "/" + resource + "/" + id, {
handleAs: "json",
headers: { "Content-Type": "application/json" },
data: JSON.stringify(data)
});
},
remove: function(resource, id) {
return request.del(BASE + "/" + resource + "/" + id);
}
};
});
// Usage in any widget:
// api.getAll("employees", { dept: "Engineering" }).then(renderList);
// api.create("employees", newEmp).then(function(e) { store.add(e); });
});
dojo/request/notify — Global Request Hooksdojo/request/notify lets you hook into every request lifecycle event globally — perfect for showing a loading spinner, logging all requests, or handling 401/403 centrally.
require(["dojo/request/notify", "dojo/topic"], function(notify, topic) {
var pendingCount = 0;
var spinner = document.getElementById("globalSpinner");
// ── Show spinner on any request start ─────────────────────────
notify("start", function(promise) {
pendingCount++;
spinner.style.display = "block";
});
// ── Hide spinner when all requests complete ────────────────────
notify("stop", function() {
// "stop" fires when ALL in-flight requests are done
pendingCount = 0;
spinner.style.display = "none";
});
// ── Log every sent request ─────────────────────────────────────
notify("send", function(response, cancel) {
// response is a promise for the response object
// cancel() aborts the request
console.log("[XHR] →", response);
});
// ── Handle every response ─────────────────────────────────────
notify("load", function(response) {
// response.status, response.url, response.text
console.log("[XHR] �?", response.status, response.url);
});
// ── Central error handler — catch 401/403/500 globally ────────
notify("error", function(error) {
console.error("[XHR] ✗", error.response && error.response.status, error.message);
if (error.response) {
switch (error.response.status) {
case 401:
topic.publish("auth/sessionExpired");
break;
case 403:
topic.publish("auth/forbidden", { url: error.response.url });
break;
case 500:
case 503:
topic.publish("app/serverError", { error: error });
break;
}
}
});
});
notify setup module once in your app bootstrap (app/boot.js) before any other requests are made. It registers globally and covers all subsequent dojo/request calls across every module.
Build a fetchWithRetry(url, maxRetries) function that:
urlmaxRetries timesprogress({ attempt: n, maxRetries: maxRetries }) on each retryrequire([
"dojo/Deferred",
"dojo/request/xhr"
], function(Deferred, xhr) {
function fetchWithRetry(url, maxRetries) {
var deferred = new Deferred();
var attempt = 0;
function attempt_() {
attempt++;
xhr.get(url, { handleAs: "json" })
.then(
function(data) {
deferred.resolve(data); // success — we're done
},
function(err) {
if (attempt < maxRetries) {
// Notify listeners of the retry
deferred.progress({ attempt: attempt, maxRetries: maxRetries, error: err });
// Wait 1s then try again (exponential backoff variant: attempt * 1000)
setTimeout(attempt_, attempt * 1000);
} else {
// All retries exhausted
err.message = "Failed after " + maxRetries + " attempts: " + err.message;
deferred.reject(err);
}
}
);
}
attempt_(); // kick off first attempt
return deferred.promise;
}
// Usage
fetchWithRetry("/api/data", 3)
.then(
function(data) {
console.log("Loaded:", data);
},
function(err) {
console.error("Permanently failed:", err.message);
},
function(progress) {
console.log("Retrying... attempt", progress.attempt, "of", progress.maxRetries);
updateRetryIndicator(progress.attempt, progress.maxRetries);
}
);
});
| Concept | Key Rule |
|---|---|
new Deferred() | Returns a Deferred. Give callers deferred.promise, keep deferred to yourself. |
deferred.resolve(v) | Fulfills — triggers .then(onFulfilled) |
deferred.reject(err) | Rejects — triggers .otherwise() or .then(null, onRejected) |
deferred.progress(v) | Notifies — triggers .then(null, null, onProgress) |
.then(fn) | Returns new promise. Always return from the callback. |
.otherwise(fn) | Catches errors — return a value to recover, throw to re-reject. |
dojo/when | Normalizes values + promises — use when callers may pass either. |
promise/all([]) | Parallel — fails fast if any rejects. |
promise/all({}) | Named parallel — same behaviour, cleaner code. |
xhr.get/post/put | handleAs:"json" auto-parses. Access raw via .response. |
request/notify | Global hooks for spinner, auth, error logging — load once at boot. |
Time: 6–7 hrs
Pages: 16–20
Exercises: 1
Patterns: retry, REST wrapper, global hooks