# How Fre Tracks Promises for Suspense Boundaries: WeakMap and Fiber Architecture

> Discover how Fre tracks promises for Suspense boundaries using WeakMap and Fiber architecture. Learn about the efficient promise handling in Frejs.

- Repository: [frejs/fre](https://github.com/frejs/fre)
- Tags: internals
- Published: 2026-03-02

---

**Fre tracks promises for Suspense boundaries by throwing promises from components during the render phase and catching them in the reconciler's `updateHook` function, then storing associations in a `WeakMap` named `suspendPromiseMap` that links each promise to its nearest Suspense boundary fiber.**

Suspense enables declarative asynchronous rendering in modern frontend frameworks, allowing components to display fallback content while waiting for data or code to load. In **Fre**—a lightweight, React-compatible library—this feature relies on a sophisticated tracking mechanism that maps promises to fiber nodes in the component tree. Understanding how Fre tracks promises for Suspense boundaries requires examining the reconciler's throw-and-catch pattern and the WeakMap-based registry in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts).

## Throwing and Catching Promises During Render

Fre adopts the throw-and-catch pattern for Suspense implementation. When a component needs to suspend—typically through the `lazy` utility or by manually throwing a promise—it throws the promise object during its execution. The reconciler wraps component rendering in a **try-catch block** inside the `updateHook` function.

In [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) (lines 61-66), when a promise is caught, the reconciler immediately invokes `suspenseRender` to handle the suspension:

```typescript
// Conceptual flow in reconcile.ts
try {
  // Component renders
} catch (e) {
  if (e instanceof Promise) {
    suspenseRender(e, fiber)
  }
}

```

This interruption prevents the component tree from completing its render until the promise resolves.

### The Role of lazy Components

The `lazy` function, defined in [`src/h.ts`](https://github.com/frejs/fre/blob/main/src/h.ts), creates components that automatically throw the promise returned by dynamic import functions. When the lazy component renders before its module has loaded, it throws the import promise, triggering the catch block in `updateHook` and initiating the Suspense workflow.

## Locating the Nearest Suspense Boundary

Once `suspenseRender` receives a thrown promise, it must identify which Suspense boundary should handle it. Fre traverses the fiber tree upward using the `getBoundary` function, searching for the nearest ancestor fiber with the Suspense type.

According to [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) (lines 46-53), this traversal walks from the current fiber to the root:

```typescript
const getBoundary = (fiber, type) => {
  while (fiber) {
    if (fiber.type === type) return fiber
    fiber = fiber.parent
  }
}

```

This ensures that nested Suspense boundaries handle only the promises thrown by their descendants, allowing fine-grained loading states.

## The WeakMap Tracking Mechanism

The core of Fre's promise tracking lies in **`suspendPromiseMap`**, a `WeakMap<Promise, Set<Fiber>>` defined in the reconciler. This structure maps each unique promise to a Set of Suspense boundary fibers that are waiting for it.

As implemented in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) (lines 75-83), the registration logic works as follows:

- If the promise is new to the map, Fre creates a new `Set` containing the boundary fiber and attaches a `.then` listener to the promise.
- If the promise already exists in the map (indicating another component or boundary is waiting for the same async operation), Fre simply adds the new boundary to the existing Set.

```typescript
// Simplified from reconcile.ts
if (!suspendPromiseMap.has(promise)) {
  suspendPromiseMap.set(promise, new Set([boundary]))
  promise.then(() => {
    // Resolution handler
  }).finally(() => {
    suspendPromiseMap.delete(promise)
  })
} else {
  suspendPromiseMap.get(promise).add(boundary)
}

```

Using a **WeakMap** ensures that resolved promises remain eligible for garbage collection once all references are cleared, preventing memory leaks in long-running applications.

## Promise Resolution and Boundary Updates

When a tracked promise resolves, the `.then` handler retrieves the `Set<Fiber>` from `suspendPromiseMap`. Before triggering updates, Fre filters out boundaries that are no longer valid—specifically those marked with **`TAG.REPLACE`** or **`TAG.REMOVE`** flags defined in [`src/type.ts`](https://github.com/frejs/fre/blob/main/src/type.ts).

As shown in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) (lines 78-82), the resolution handler iterates through the boundary set and schedules updates only for alive fibers:

```typescript
const boundaries = suspendPromiseMap.get(promise)
boundaries.forEach(boundary => {
  if (!(boundary.tag & (TAG.REPLACE | TAG.REMOVE))) {
    update(boundary)
  }
})

```

This filtering prevents unnecessary re-renders of components that have been unmounted or replaced while the promise was pending.

## Swapping Fallback for Primary Content

During the initial suspension, `suspenseRender` immediately renders the **fallback UI** while keeping the primary children in an off-screen fragment using **`MODE.OFFSCREEN`**. When the promise resolves and `update` triggers the re-render, `suspenseRender` (lines 64-74 in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts)) swaps these fragments.

The function returns `boundary.child`, allowing the reconciler to proceed with rendering the actual component tree instead of the fallback. This transition occurs seamlessly because the promise resolution has guaranteed that the suspended component can now render without throwing.

## Practical Implementation with Lazy Loading

To see promise tracking in action, consider a typical lazy loading pattern:

```tsx
// demo/src/suspense.tsx
import { Suspense, lazy } from 'fre';

const AsyncComp = lazy(() => import('./async-comp'));

export default function App() {
  return (
    <Suspense fallback={<p>Loading…</p>}>
      <AsyncComp />
    </Suspense>
  );
}

```

In this example, `AsyncComp` throws the dynamic import promise on its first render. The reconciler catches this promise, registers the `<Suspense>` boundary in `suspendPromiseMap`, and renders "Loading…". Once the import resolves, the boundary updates and Fre renders the loaded component.

## Summary

- Fre intercepts promises thrown during component rendering in the `updateHook` **try-catch block** located in [`src/reconcile.ts`](https://github.com/frejs/fre/blob/main/src/reconcile.ts) (lines 61-66).
- The framework locates the nearest Suspense boundary by traversing the fiber tree upward via `getBoundary`.
- Promise-to-boundary associations are stored in **`suspendPromiseMap`**, a `WeakMap` mapping promises to Sets of fiber nodes, supporting multiple boundaries per promise.
- When promises resolve, Fre filters out stale boundaries using **TAG flags** before calling `update` to schedule re-renders.
- The reconciler swaps **off-screen fragments** for the primary content once promises settle, completing the Suspense transition.

## Frequently Asked Questions

### What data structure does Fre use to track Suspense promises?

Fre uses a **`WeakMap<Promise, Set<Fiber>>`** named `suspendPromiseMap`. The WeakMap keys are the thrown promises, and the values are Sets containing one or more Suspense boundary fibers that are waiting for that specific promise to resolve.

### How does Fre prevent memory leaks when tracking promises?

The use of a **WeakMap** ensures that promise references do not prevent garbage collection. Once a promise resolves and the `.finally` handler deletes the entry from `suspendPromiseMap`, the promise becomes eligible for garbage collection if no other references exist. Additionally, resolved promises are automatically removed from the map after triggering boundary updates.

### Can multiple Suspense boundaries track the same promise simultaneously?

Yes. When a promise is thrown within a component tree containing nested Suspense boundaries, or when multiple components throw the same promise reference, Fre adds each relevant boundary fiber to the **Set** associated with that promise in `suspendPromiseMap`. When the promise resolves, all boundaries in the Set receive update notifications.

### How does Fre determine if a Suspense boundary is still valid when a promise resolves?

Before re-rendering boundaries, Fre checks the fiber's **tag** property against the `TAG.REPLACE` and `TAG.REMOVE` flags defined in [`src/type.ts`](https://github.com/frejs/fre/blob/main/src/type.ts). Boundaries marked with these flags—indicating they have been replaced or removed from the tree—are filtered out and skip the update, ensuring only active boundaries re-render when promises settle.