Menu
Public repository / 91bd2a37654909c5a3aef6ddd614991bd378ccb8
AkurAI-Framework / site/content/docs/boundaries.md
Latest change 4c854a168e94887cd3c9ba5ec2150c94c7bda235 - feat(batch8-c): add loading and error boundaries for SPA nav and async data by Ólafur Búi Ólafsson
# Loading & Error Boundaries
> Graceful UI for SPA navigation and async data fetching. No dependencies, no build step.
## Overview
The boundaries module provides two layers of protection for your SPA:
1. **Navigation boundary** — A top-of-page progress bar that shows when the SPA router is navigating between pages.
2. **Data boundary** — A `withBoundary()` helper that wraps async data fetches in graceful loading and error UI, with built-in Retry.
Both are **progressive enhancements**: if JavaScript is disabled or the script fails, navigation still works (via full-page reloads) and data fetches silently fail (no broken UI).
## Installation
Add `boundaries.js` to your page, ideally before the `app.js` loader:
```html
<script type="module" src="/boundaries.js"></script>
```
The module initializes itself on load and exports the `SpaNavBoundary` global with two APIs:
- `SpaNavBoundary.withBoundary(el, asyncFn, opts)` — Wrap an async data fetch
- `SpaNavBoundary.startNavProgress()` — Manually trigger the progress bar
- `SpaNavBoundary.endNavProgress()` — Manually end it
## Navigation Boundary
The SPA router (`spa.js`) intercepts same-origin link clicks and fetches pages asynchronously. The boundaries module adds a **progress bar** that animates from left to right during the fetch.
### How it works
1. User clicks a link
2. Boundaries detects the click (via capturing listener) and shows the progress bar
3. `spa.js` fetches the page
4. SPA router emits `spa:navigated` event
5. Boundaries catches `spa:navigated` and hides the progress bar
### Styling
The progress bar is injected at the top of the page with these inline styles:
- Position: fixed, top, height 3px
- Gradient from `--accent` to `--accent-2`
- Opacity 0 by default; fades in when navigating
You can override it with CSS:
```css
.spa-nav-progress {
/* your overrides */
background: #your-color !important;
height: 4px !important;
}
```
### Manual control
If you want to trigger the progress bar without SPA navigation:
```js
SpaNavBoundary.startNavProgress();
// ... do something async
SpaNavBoundary.endNavProgress();
```
## Data Boundary
The `withBoundary()` helper wraps async functions (typically `fetch()` calls) in three states:
1. **Loading** — Shows a spinner and "Loading…" message
2. **Success** — Renders the returned content
3. **Error** — Shows an error banner with a Retry button
### Basic usage
```js
const el = document.getElementById("data-container");
const fetchData = async () => {
const res = await fetch("/api/data");
if (!res.ok) throw new Error(`status ${res.status}`);
return res.text(); // HTML string to render
};
SpaNavBoundary.withBoundary(el, fetchData, {
loading: true, // show spinner while fetching
error: true // show error banner on failure
});
```
### Return types
The async function can return:
- **HTML string** — `res.text()` or `"<div>content</div>"`
- **HTMLElement** — A DOM node created with `document.createElement()`
- **Any other value** — Will be coerced to string and rendered
```js
// Return HTML string
SpaNavBoundary.withBoundary(el, async () => {
const res = await fetch("/api/html");
return res.text();
});
// Return DOM element
SpaNavBoundary.withBoundary(el, async () => {
const div = document.createElement("div");
div.className = "card";
div.textContent = "Loaded!";
return div;
});
```
### Options
The third argument controls loading/error UI:
```js
{
loading: true, // (default) show a spinner while fetching
error: true // (default) show an error banner with Retry
}
```
Set either to `false` to hide that state:
```js
// No loading spinner, just fail silently on error
SpaNavBoundary.withBoundary(el, asyncFn, {
loading: false,
error: false
});
```
### Error handling
When the async function rejects:
1. First retry: shows error banner + Retry button
2. Second retry: shows error without button (to prevent infinite loops)
The error message is extracted from the caught exception:
```js
// Custom error message
throw new Error("Failed to load user data");
// → displays in the banner as "Failed to load user data"
```
### Retry behavior
The Retry button re-runs the entire boundary:
```js
SpaNavBoundary.withBoundary(el, asyncFn, {
loading: true,
error: true
});
// User clicks Retry → calls withBoundary again
```
This clears both the error state and the `isRetry` flag, allowing fresh attempts.
## Custom Loading States
By default, `withBoundary()` shows a centered spinner. To use custom loading UI (e.g., skeleton screens), pre-populate the container before calling the boundary:
```js
// Custom skeleton loading
const el = document.getElementById("content");
el.innerHTML = `
<div class="skeleton skeleton-card"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line skeleton-line-sm"></div>
`;
// Disable the default spinner
SpaNavBoundary.withBoundary(el, asyncFn, {
loading: false, // don't show default spinner
error: true // but still show error UI
});
```
The boundary will replace your skeleton with real content once the fetch completes.
## Accessibility
Boundaries integrate `aria-busy` and semantic roles for screen readers:
- **Loading** — `aria-busy="true"` on the container
- **Error** — `role="alert"` on the banner (via CSS `.banner`)
- **Status** — `role="status"` on the loading message
## Console logging
All boundaries emit `[boundaries]` breadcrumb logs:
```
[boundaries] initialized (SPA nav boundaries + withBoundary helper)
[boundaries] navigation start
[boundaries] showing loading state
[boundaries] content rendered
[boundaries] navigation end
[boundaries] fetch error: Network error
[boundaries] showing error UI: Network error
```
Open DevTools → Console to debug loading/error states in real time.
## Progressive enhancement
The entire boundaries module is a progressive enhancement:
- **Without JavaScript**: links still work (full-page navigation), form submissions work
- **With JavaScript disabled**: no spinners, no error banners, no fancy UX
- **If boundaries.js fails to load**: `spa.js` still works, but you lose the progress bar and error UI
This is why `withBoundary()` doesn't throw on init — the page keeps functioning even if the boundary is absent.
## Demo
Visit [/boundaries-demo](/boundaries-demo) to see loading boundaries and error handling in action.
## Performance
- **Lazy init** — The module only adds listeners when `document.readyState` is "interactive" or later
- **Minimal overhead** — One progress bar DOM element, one listener per navigation, one per async boundary
- **No dependencies** — Pure vanilla JS; zero third-party code
## Browser support
Requires:
- `fetch()` API
- `CustomEvent` (for `spa:navigated` listening)
- `document.readyState` (for init guarding)
Gracefully degrades on very old browsers (IE9 and earlier).