PHASE 5

Async & Deferred

Dojo's promise system — Deferred, chaining, parallel ops, request/xhr, and production error handling patterns

Deferred Promise Chain dojo/request dojo/when promise/all Error Handling
What you'll understand by the end: How Dojo's Deferred 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.
1

The Callback Hell Problem

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:

  • Return an object from async functions instead of accepting a callback parameter
  • Chain operations flat instead of nesting them
  • Single error handler catches failures anywhere in the chain
// 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
2

dojo/Deferred — Resolve, Reject, Progress

A 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.

new Deferred() pending // async operation in flight ↓ resolve(value) ↓ reject(error) resolved rejected ↓ .then() fires ↓ .otherwise() fires consumer gets value consumer gets error
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
});

Deferred vs Native Promise — Key Differences

dojo/Deferred (1.17)Native Promise (ES6)
ProgressYes — deferred.progress(v)No built-in progress
CancellationYes — deferred.cancel()No (AbortController is separate)
Sync resolveYes — resolve() before .then()Always async (microtask)
Error keyword.otherwise(fn).catch(fn)
Interopdojo/when wraps native PromisesNeeds wrapper for Dojo chains
3

.then() Chaining

Each .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);
    });
});
⚠ GOTCHA — Forgetting to Return
If you forget 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" });
})
4

.otherwise() — Error Handling

require(["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);
});
5

dojo/when — Values & Promises Unified

dojo/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();
});
6

dojo/promise/all — Parallel Operations

dojo/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);
    });
});
Performance tip: Always ask: "Do these requests depend on each other?" If no — use 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.
7

dojo/request/xhr — HTTP Requests

require(["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
  });
});
8

dojo/request — Unified API

dojo/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); });
});
9

dojo/request/notify — Global Request Hooks

dojo/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;
      }
    }
  });
});
Architecture tip: Load your 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.

🔧 Exercise 5.10 — Retry Queue with Deferred Chaining

Build a fetchWithRetry(url, maxRetries) function that:

  • Makes an XHR request to url
  • If it fails, waits 1 second and retries up to maxRetries times
  • Returns a promise that resolves with the data or rejects after all retries are exhausted
  • Reports progress: progress({ attempt: n, maxRetries: maxRetries }) on each retry
require([
  "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);
      }
    );
});

Phase 5 — Quick Reference

ConceptKey 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/whenNormalizes 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/puthandleAs:"json" auto-parses. Access raw via .response.
request/notifyGlobal hooks for spinner, auth, error logging — load once at boot.

Ready for Phase 6?

Async covered. Next: decoupling your modules completely with dojo/topic pub/sub — so widgets communicate without holding references to each other.

Continue to Phase 6: Pub/Sub Architecture →