---
title: "JSX Streaming and Suspense"
description: "Hono provides experimental JSX streaming and Suspense capabilities via renderToReadableStream and the Suspense component, enabling server-rendered HTML chunks to be flushed progressively to clients..."
last_updated: "2026-10-12T08:14:12.740617+00:00"
canonical_url: "https://www.doc0.dev/docs/552ca36e-f67e-41c3-a07a-def9bd9551b0/technical/jsx-and-ui/jsx-streaming-and-suspense"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [src/jsx/streaming.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts)
- [src/jsx/components.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/components.ts)
- [src/jsx/dom/render.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/render.ts)
- [src/jsx/dom/components.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts)
- [src/jsx/dom/intrinsic-element/components.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/intrinsic-element/components.ts)
- [src/jsx/dom/server.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/server.ts)
- [src/jsx/intrinsic-element/components.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/intrinsic-element/components.ts)
- [src/jsx/hooks/index.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/hooks/index.ts)
- [src/helper/streaming/sse.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/helper/streaming/sse.ts)
- [src/jsx/jsx-runtime.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/jsx-runtime.ts)
- [src/jsx/base.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/base.ts)
- [src/middleware/jsx-renderer/index.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/middleware/jsx-renderer/index.ts)
- [src/helper/streaming/text.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/helper/streaming/text.ts)
- [src/helper/streaming/stream.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/helper/streaming/stream.ts)
- [src/jsx/index.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/index.ts)
- [src/jsx/dom/hooks/index.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/hooks/index.ts)
- [src/jsx/dom/index.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/index.ts)
- [src/jsx/dom/client.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/client.ts)
- [src/jsx/jsx-dev-runtime.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/jsx-dev-runtime.ts)
- [src/utils/stream.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/utils/stream.ts)
- [src/utils/html.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/utils/html.ts)
- [src/middleware/etag/digest.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/middleware/etag/digest.ts)
- [src/jsx/constants.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/constants.ts)
- [src/jsx/dom/jsx-dev-runtime.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/jsx-dev-runtime.ts)
- [runtime-tests/lambda/stream-mock.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/runtime-tests/lambda/stream-mock.ts)
- [src/jsx/dom/jsx-runtime.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/jsx-runtime.ts)
- [src/jsx/intrinsic-elements.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/intrinsic-elements.ts)
- [src/adapter/cloudflare-workers/utils.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/adapter/cloudflare-workers/utils.ts)
- [runtime-tests/lambda/mock.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/runtime-tests/lambda/mock.ts)
- [src/jsx/intrinsic-element/common.ts](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/intrinsic-element/common.ts)
</details>

## Overview

Hono provides experimental JSX streaming and Suspense capabilities via `renderToReadableStream` and the `Suspense` component, enabling server-rendered HTML chunks to be flushed progressively to clients. By resolving asynchronous child nodes and callbacks across execution phases, it allows fallback content placeholders to be emitted immediately while pending components resolve out-of-order.

Sources: [src/jsx/streaming.ts:42-133](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L42-L133), [src/jsx/streaming.ts:142-201](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L142-L201)

## Stream Generation and ReadableStream Pipeline

Hono exposes stream generation functions across both core JSX modules and React DOM server compatibility layers. The primary streaming entry point is `renderToReadableStream`, defined in `src/jsx/streaming.ts`, which accepts an initial content payload and an optional error handler before constructing a standard `ReadableStream<Uint8Array>` instance. The `hono/jsx/server` module (`src/jsx/dom/server.ts`) re-exports an asynchronous variant of `renderToReadableStream` that delegates directly to the core streaming implementation while parsing configuration options such as `onError`.

```typescript
export const renderToReadableStream = (
  content: HtmlEscapedString | JSXNode | Promise<HtmlEscapedString>,
  onError: (e: unknown) => string | void = console.trace
): ReadableStream<Uint8Array> => {
  let cancelled = false
  const reader = new ReadableStream<Uint8Array>({
    async start(controller) {
      try {
        if (content instanceof JSXNode) {
          content = content.toString() as HtmlEscapedString | Promise<HtmlEscapedString>
        }
        const context = typeof content === 'object' ? content : {}
        const resolved = await resolveCallback(
          content,
          HtmlEscapedCallbackPhase.BeforeStream,
          true,
          context
        )
        if (!cancelled) {
          controller.enqueue(textEncoder.encode(resolved))
        }
// ...
```

Sources: [src/jsx/streaming.ts:142-163](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L142-L163)

The pipeline transforms JSX nodes, escaped strings, or promises into binary chunks using a `TextEncoder` instance. When `renderToReadableStream` initializes, it checks whether the content parameter is an instance of `JSXNode`, invoking its `.toString()` method to yield an escaped string or promise. It then resolves callbacks through `HtmlEscapedCallbackPhase.BeforeStream` before enqueuing the encoded byte array into the readable stream controller.

| Function / Module | Target Runtime / API | Parameters | Return Type |
| :--- | :--- | :--- | :--- |
| `renderToReadableStream` (`src/jsx/streaming.ts`) | Core Hono JSX Streaming | `content`, `onError` | `ReadableStream<Uint8Array>` |
| `renderToReadableStream` (`src/jsx/dom/server.ts`) | React DOM Server Compatibility | `element`, `options` | `Promise<ReadableStream<Uint8Array>>` |
| `renderToString` (`src/jsx/dom/server.ts`) | Synchronous DOM Server | `element`, `options` | `string` |

Sources: [src/jsx/streaming.ts:142-145](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L142-L145), [src/jsx/dom/server.ts:21-30](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/server.ts#L21-L30), [src/jsx/dom/server.ts:50-63](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/server.ts#L50-L63)

> [!NOTE]
> Options passed to `renderToReadableStream` in `hono/jsx/server` other than `onError` (such as `identifierPrefix`, `namespaceURI`, `nonce`, `bootstrapScripts`, and `progressiveChunkSize`) trigger a console warning stating that those options are not supported yet.

Sources: [src/jsx/dom/server.ts:54-56](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/server.ts#L54-L56)

> [!WARNING]
> Synchronous rendering via `renderToString` in `hono/jsx/server` throws an error if an asynchronous component is encountered, as async components cannot be resolved without a Promise-based [execution context](https://www.doc0.dev/docs/552ca36e-f67e-41c3-a07a-def9bd9551b0/technical/core-engine/execution-context).

Sources: [src/jsx/dom/server.ts:26-28](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/server.ts#L26-L28)

## Suspense Architecture and Placeholder Inlining

### Component Structure and Context Resolution

The `Suspense` component in `src/jsx/streaming.ts` acts as an asynchronous boundary resolver for JSX tree rendering. When evaluated, it wraps child elements and checks context for script nonces if provided through custom providers or context maps. Children passed to `Suspense` are normalized into an array, and the component initializes a data stack node utilizing `DOM_STASH` to support the execution of hooks and asynchronous boundaries.

```typescript
  const stackNode = { [DOM_STASH]: [0, []] } as unknown as NodeObject
  const popNodeStack = (value?: unknown) => {
    buildDataStack.pop()
    return value
  }
```
Sources: [src/jsx/streaming.ts:55-59](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L55-L59)

### Fallback Marker Emission and Promise Detection

During synchronous child stringification, if a child evaluation throws a `Promise` (triggered by an asynchronous component or data-fetching hook), the `Suspense` boundary captures the promise and switches into streaming mode. 

```typescript
  try {
    stackNode[DOM_STASH][0] = 0
    buildDataStack.push([[], stackNode])
    resArray = children.map((c) =>
      c == null || typeof c === 'boolean' ? '' : c.toString()
    ) as HtmlEscapedString[]
  } catch (e) {
    if (e instanceof Promise) {
      resArray = [
        e.then(() => {
          stackNode[DOM_STASH][0] = 0
          buildDataStack.push([[], stackNode])
          return childrenToString(children as Child[]).then(popNodeStack)
        }),
      ] as Promise<HtmlEscapedString[]>[]
    } else {
      throw e
    }
  } finally {
    popNodeStack()
  }
```
Sources: [src/jsx/streaming.ts:61-81](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L61-L81)

When asynchronous promises are detected in `resArray`, a unique numeric identifier is assigned using the module-level `suspenseCounter`. The fallback content is serialized and wrapped in an HTML comment boundary and a template anchor tag using the format `<template id="H:${index}"></template>${fallbackStr}<!--/$-->`.

```typescript
  if (resArray.some((res) => (res as {}) instanceof Promise)) {
    const index = suspenseCounter++
    const fallbackStr = await fallback.toString()
    return raw(`<template id="H:${index}"></template>${fallbackStr}<!--/$-->`, [
      ...(fallbackStr.callbacks || []),
      ({ phase, buffer, context }) => {
// ...
```
Sources: [src/jsx/streaming.ts:83-88](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L83-L88)

> [!NOTE]
> If all child nodes resolve synchronously without throwing a `Promise`, `Suspense` bypasses streaming mechanics entirely, returning the concatenated string result via `raw(resArray.join(''))`.

Sources: [src/jsx/streaming.ts:130-132](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L130-L132)

> [!WARNING]
> The DOM-specific counterpart `Suspense` exported from `src/jsx/dom/components.ts` registers a `DOM_ERROR_HANDLER` that catches promise rejections differently, attaching a `.finally(retry)` hook for client-side DOM reconciliation rather than server-side template streaming.

Sources: [src/jsx/dom/components.ts:25-38](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts#L25-L38)

## Out-of-Order Chunk Resolution and Replacement

### Callback Phases and Stream Buffering

When asynchronous promises within a `Suspense` boundary complete, the callback mechanism handles chunk resolution based on the `HtmlEscapedCallbackPhase`. During the initial `BeforeStream` phase, callbacks return early without altering the stream output. Once the stream begins, `Promise.all(resArray)` resolves all pending asynchronous children, flattening the resulting HTML array into a unified string.

```typescript
        return Promise.all(resArray).then(async (htmlArray) => {
          htmlArray = htmlArray.flat()
          const content = htmlArray.join('')
          if (buffer) {
            buffer[0] = buffer[0].replace(
              new RegExp(`<template id="H:${index}"></template>.*?<!--/\\$-->`),
              content
            )
          }
```
Sources: [src/jsx/streaming.ts:92-100](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L92-L100)

If an active chunk buffer exists (`buffer`), the runtime replaces the placeholder region—bounded by `<template id="H:${index}"></template>` and the closing comment `<!--/$-->`—directly within the buffered content string. When no buffer is present, the resolved content is wrapped in a target template and paired with an inline swapping script for client-side insertion.

> [!NOTE]
> The buffer check allows server-side renderers to catch up and inline resolved components directly into preceding stream chunks if resolution finishes before the chunk is flushed to the network controller.

Sources: [src/jsx/streaming.ts:95-103](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L95-L103)

### Client-Side DOM Swapping Scripts

When streaming responses without an active pre-buffer, Hono injects an IIFE execution script immediately following the resolved chunk container. This script locates the placeholder anchor via `document.getElementById('H:${index}')`, traverses sibling nodes up to the closing comment boundary (`/^\$/`), and replaces the fallback content with the new template content.

```typescript
          let html = buffer
            ? ''
            : `<template data-hono-target="H:${index}">${content}</template><script${
                nonce ? ` nonce="${nonce}"` : ''
              }>
((d,c,n) => {
c=d.currentScript.previousSibling
d=d.getElementById('H:${index}')
if(!d)return
do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
d.replaceWith(c.content)
})(document)
</script>`
```
Sources: [src/jsx/streaming.ts:101-113](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L101-L113)

> [!WARNING]
> If a script nonce is resolved from context and configured, it is appended directly to the injected `<script>` tag. Omission of a valid nonce under strict Content Security Policy (CSP) headers will block the swapping script from executing in the browser.

Sources: [src/jsx/streaming.ts:103-105](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L103-L105)

### Nested Callback Resolution and Recursion

Resolved HTML chunks may contain their own nested callbacks. The callback processor inspects each resolved fragment for embedded callback arrays, recursively resolving nested stream phases before returning the final raw escaped string block.

```typescript
          const callbacks = htmlArray
            .map((html) => (html as HtmlEscapedString).callbacks || [])
            .flat()
          if (!callbacks.length) {
            return html
          }

          if (phase === HtmlEscapedCallbackPhase.Stream) {
            html = await resolveCallback(html, HtmlEscapedCallbackPhase.BeforeStream, true, context)
          }

          return raw(html, callbacks)
```
Sources: [src/jsx/streaming.ts:115-128](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L115-L128)

## Client-Side DOM Suspense Compatibility

### Suspense Boundary Definition and Promise Detection

In client-side DOM rendering environments provided by `hono/jsx/dom`, the `Suspense` component wraps child trees using `Fragment` and attaches a custom error handler via the `DOM_ERROR_HANDLER` constant. When rendering client-side nodes, asynchronous operations throw promises that trigger this handler. The boundary inspects the caught error to determine whether it represents an ongoing asynchronous task or an unhandled exception.

```typescript
export const Suspense: FC<PropsWithChildren<{ fallback: any }>> = (({
  children,
  fallback,
}: any) => {
  const res = Fragment({ children })
  ;(res as any)[DOM_ERROR_HANDLER] = (err: any, retry: () => void) => {
    if (!(err instanceof Promise)) {
      throw err
    }
    err.finally(retry)
    return fallback
  }
  return res
}) as any
```
Sources: [src/jsx/dom/components.ts:25-38](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts#L25-L38)

> [!NOTE]
> If the caught error is not an instance of a `Promise`, the `Suspense` handler rethrows the error immediately, passing control up to the nearest `ErrorBoundary` or global handler.

Sources: [src/jsx/dom/components.ts:31-33](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts#L31-L33)

### ErrorBoundary Integration and Fallback Rendering

Client-side component trees also utilize `ErrorBoundary` to manage non-promise exceptions and trigger fallback rendering views. The error boundary wraps children in a fragment and attaches a `DOM_ERROR_HANDLER` that invokes optional error callbacks and resolves fallback UI nodes.

```typescript
export const ErrorBoundary: FC<
  PropsWithChildren<{
    fallback?: Child
    fallbackRender?: FallbackRender
    onError?: ErrorHandler
  }>
> = (({ children, fallback, fallbackRender, onError }: any) => {
  const res = Fragment({ children })
  ;(res as any)[DOM_ERROR_HANDLER] = (err: any) => {
    if (err instanceof Promise) {
      throw err
    }
    onError?.(err)
    return fallbackRender?.(err) || fallback
  }
  return res
}) as any
```
Sources: [src/jsx/dom/components.ts:7-23](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts#L7-L23)

> [!IMPORTANT]
> Unlike `Suspense`, which catches promises and schedules a retry upon resolution, `ErrorBoundary` explicitly rethrows any encountered `Promise` instances, allowing asynchronous boundaries to handle data-fetching suspensions further up or down the tree.

Sources: [src/jsx/dom/components.ts:16-18](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts#L16-L18)

## Lifecycle Coordination and Error Handling

### Error Catching and Recovery in Readable Streams

During the execution of `renderToReadableStream`, asynchronous chunk generation and callback resolution can fail. When a promise returned by a callback rejects, the error handling logic catches the exception, logs it via `console.log`, invokes the user-provided `onError` callback function, and returns an empty string to prevent stream termination.

```typescript
        const then = (promise: Promise<string>) => {
          callbacks.push(
            promise
              .catch((err) => {
                console.log(err)
                onError(err)
                return ''
              })
              .then(async (res) => {
```
Sources: [src/jsx/streaming.ts:167-175](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L167-L175)

> [!CAUTION]
> The default error handler for `renderToReadableStream` is `console.trace`, which outputs stack traces to standard output. Production environments should supply a custom `onError` handler to manage error telemetry and status propagation properly.

Sources: [src/jsx/streaming.ts:144-144](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/streaming.ts#L144-L144)

### Component-Level Error Boundaries and Suspense Retry Logic

Client-side DOM component rendering coordinates errors through boundaries defined with `DOM_ERROR_HANDLER`. The `Suspense` component distinguishes between pending promises and synchronous exceptions. If an error is a promise, it attaches a `finally` handler to trigger a `retry` execution, while displaying the fallback UI. Conversely, an `ErrorBoundary` executes an optional `onError` callback and renders either `fallbackRender` or a static `fallback` node.

```typescript
export const ErrorBoundary: FC<
  PropsWithChildren<{
    fallback?: Child
    fallbackRender?: FallbackRender
    onError?: ErrorHandler
  }>
> = (({ children, fallback, fallbackRender, onError }: any) => {
  const res = Fragment({ children })
  ;(res as any)[DOM_ERROR_HANDLER] = (err: any) => {
    if (err instanceof Promise) {
      throw err
    }
    onError?.(err)
    return fallbackRender?.(err) || fallback
  }
  return res
}) as any
```
Sources: [src/jsx/dom/components.ts:7-23](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts#L7-L23)

> [!NOTE]
> `ErrorBoundary` automatically rethrows any caught error if it is an instance of a `Promise`, ensuring that async data-loading errors are correctly handled by parent `Suspense` boundaries rather than being swallowed as rendering faults.

Sources: [src/jsx/dom/components.ts:16-18](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/jsx/dom/components.ts#L16-L18)

## Related

- [JSX Runtime](https://www.doc0.dev/docs/552ca36e-f67e-41c3-a07a-def9bd9551b0/technical/jsx-and-ui/jsx-runtime)
- [Streaming and SSE](https://www.doc0.dev/docs/552ca36e-f67e-41c3-a07a-def9bd9551b0/technical/helpers-and-tooling/streaming-and-sse)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/552ca36e-f67e-41c3-a07a-def9bd9551b0/llms.txt) for all pages in this wiki.
