A dashboard loads several independent pieces of data and shows each one in its own widget. One failing widget should not break the others, which is the perfect job for Promise.allSettled.
<style>
.dash { font-family: system-ui, sans-serif; max-width: 420px; }
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.widgets { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; }
.widget { border: 1px solid #ddd; border-radius: 10px; padding: 10px; min-height: 70px; }
.widget h3 { margin: 0 0 4px; font-size: 13px; color: #555; font-weight: 600; }
.value { font-size: 26px; font-weight: 700; }
.widget.error { background: #fdecea; color: #b00020; }
.widget.loading { opacity: .55; }
button { font: inherit; padding: 6px 12px; cursor: pointer; }
</style>
<div class="dash">
<div class="toolbar">
<strong>Shop overview</strong>
<button id="refresh">Refresh</button>
</div>
<div class="widgets" id="widgets"></div>
<p id="updated" role="status"></p>
</div>
<script>
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
// Pretend API endpoints. The "reviews" one fails on every second refresh.
let refreshCount = 0;
const api = {
orders: async () => { await wait(300); return { value: 128 + refreshCount }; },
revenue: async () => { await wait(500); return { value: "$" + (4210 + refreshCount * 35) }; },
visitors: async () => { await wait(200); return { value: 1873 + refreshCount * 12 }; },
reviews: async () => { await wait(400); if (refreshCount % 2 === 1) throw new Error("Service unavailable"); return { value: "4.6 / 5" }; }
};
const widgetsEl = document.querySelector("#widgets");
const titles = { orders: "Orders", revenue: "Revenue", visitors: "Visitors", reviews: "Reviews" };
function widget(key, state, text) {
let node = widgetsEl.querySelector(`[data-key="${key}"]`);
if (!node) {
node = document.createElement("div");
node.dataset.key = key;
node.append(document.createElement("h3"), document.createElement("div"));
widgetsEl.append(node);
}
node.className = "widget " + state;
node.children[0].textContent = titles[key];
node.children[1].className = "value";
node.children[1].textContent = text;
}
async function refresh() {
const keys = Object.keys(api);
const button = document.querySelector("#refresh");
button.disabled = true;
keys.forEach(key => widget(key, "loading", "…"));
// start all four at once and wait for every one to settle
const results = await Promise.allSettled(keys.map(key => api[key]()));
results.forEach((result, i) => {
if (result.status === "fulfilled") widget(keys[i], "ok", result.value.value);
else widget(keys[i], "error", "Could not load");
});
const failed = results.filter(r => r.status === "rejected").length;
document.querySelector("#updated").textContent =
"Updated " + new Date().toLocaleTimeString() + (failed ? ` (${failed} widget failed)` : "");
refreshCount++;
button.disabled = false;
}
document.querySelector("#refresh").addEventListener("click", refresh);
refresh();
</script>
Press Refresh a few times: every second refresh, the Reviews widget fails while the others still update.
The key idea
const results = await Promise.allSettled(keys.map(key => api[key]()));
- All requests start in parallel, so the total time is about the slowest request, not the sum (Sequential vs Parallel).
allSettlednever rejects. Each result saysfulfilledorrejected, so each widget handles its own outcome (Promise.allSettled).Promise.allwould fail the whole dashboard at the first error. That is the wrong choice here.
Other design points
- Each widget has its own loading, ok and error state (Loading States).
- The widgets are created once and updated, not rebuilt, so the layout does not jump.
- A status line announces the last update and any failures, in text.
- The refresh button is disabled while loading.
With real endpoints
Replace each api function with a fetch call that checks response.ok and returns the parsed JSON, ideally with a shared helper (A Practical fetch() Helper).
Try changing it
- Refresh automatically every 30 seconds, pausing while the tab is hidden.
- Add a retry button on failed widgets.
- Add a timeout to each request with
AbortSignal.timeout(). - Draw a tiny trend line for each widget using an inline SVG.