Relevant source files
The following files were used as context for generating this wiki page:
- src/jsx/streaming.ts
- src/jsx/components.ts
- src/jsx/dom/render.ts
- src/jsx/dom/components.ts
- src/jsx/dom/intrinsic-element/components.ts
- src/jsx/dom/server.ts
- src/jsx/intrinsic-element/components.ts
- src/jsx/hooks/index.ts
- src/helper/streaming/sse.ts
- src/jsx/jsx-runtime.ts
- src/jsx/base.ts
- src/middleware/jsx-renderer/index.ts
- src/helper/streaming/text.ts
- src/helper/streaming/stream.ts
- src/jsx/index.ts
- src/jsx/dom/hooks/index.ts
- src/jsx/dom/index.ts
- src/jsx/dom/client.ts
- src/jsx/jsx-dev-runtime.ts
- src/utils/stream.ts
- src/utils/html.ts
- src/middleware/etag/digest.ts
- src/jsx/constants.ts
- src/jsx/dom/jsx-dev-runtime.ts
- runtime-tests/lambda/stream-mock.ts
- src/jsx/dom/jsx-runtime.ts
- src/jsx/intrinsic-elements.ts
- src/adapter/cloudflare-workers/utils.ts
- runtime-tests/lambda/mock.ts
- src/jsx/intrinsic-element/common.ts
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, src/jsx/streaming.ts:142-201
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.
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
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, src/jsx/dom/server.ts:21-30, src/jsx/dom/server.ts:50-63
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
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.
Sources: src/jsx/dom/server.ts:26-28
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.
const stackNode = { [DOM_STASH]: [0, []] } as unknown as NodeObject
const popNodeStack = (value?: unknown) => {
buildDataStack.pop()
return value
}Sources: src/jsx/streaming.ts:55-59
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.
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
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}<!--/$-->.
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
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
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
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.
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
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
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.
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
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
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.
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
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.
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 anySources: src/jsx/dom/components.ts:25-38
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
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.
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 anySources: src/jsx/dom/components.ts:7-23
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
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.
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
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
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.
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 anySources: src/jsx/dom/components.ts:7-23
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
Related
On This Page
OverviewStream Generation and ReadableStream PipelineSuspense Architecture and Placeholder InliningComponent Structure and Context ResolutionFallback Marker Emission and Promise DetectionOut-of-Order Chunk Resolution and ReplacementCallback Phases and Stream BufferingClient-Side DOM Swapping ScriptsNested Callback Resolution and RecursionClient-Side DOM Suspense CompatibilitySuspense Boundary Definition and Promise DetectionErrorBoundary Integration and Fallback RenderingLifecycle Coordination and Error HandlingError Catching and Recovery in Readable StreamsComponent-Level Error Boundaries and Suspense Retry LogicRelated