---
title: Error handling
---

# Error handling

Errors throw or reject bare. There is no component-level boundary that catches
them mid-tree: once HTML is sent, nothing can replace it, so a server error is a
500 or a fallback page. For streaming, `@vincle/flow` recovers per fragment with
`onError`.

## Top-level errors

Wrap `renderToString` in try/catch for a global fallback:

```tsx
import { renderToString } from "@vincle/core";

try {
  const html = await renderToString(<Page />);
  // send html to client
} catch (error) {
  console.error("Page render failed:", error);
  // send a 500 or fallback page
}
```

The `try` catches errors both while `<Page />` is constructed and while it
renders; a `.catch()` on the returned promise misses the first kind.

An `Error` thrown by a component is prefixed with the name of the innermost
component that threw: `[Profile] not found`. Any other thrown value reaches
`catch` exactly as thrown.

## Message format

Every error vincle raises says where it came from, what went wrong, why, and
how to fix it:

```
[vincle/<package>] <api or component>: <what happened>. <why>. <how to fix it>
```

```
[vincle/flow] <Defer target="hero">: merge="prepend" is not supported by this adapter: it supports: replace, append. Pick one of those, or use an adapter that supports "prepend".
```

The prefix (`[vincle/core]`, `[vincle/flow]`, `[vincle/vite-plugin]`,
`[vincle/precompile]`) is stable, for filtering logs. In code, read
`error.code` instead of the message.

## Error codes

Every error the libraries throw at render or setup carries a stable `code`, following Node's convention.
The type alone cannot tell a vincle refusal from a bug in your own component:

```ts
try {
  html = await renderToString(<Page />);
} catch (error) {
  if (error.code === "ERR_VINCLE_VOID_CHILDREN") {
    // the markup is wrong, not the data
  }
}
```

The codes are not exported: compare against the string, as you would an
`ENOENT`. The set may grow; an existing code will not be renamed or reused.

### `@vincle/core`

| Code                          | Raised when                                                       |
| ----------------------------- | ----------------------------------------------------------------- |
| `ERR_VINCLE_INVALID_TAG`      | a tag name that cannot be written as HTML                         |
| `ERR_VINCLE_VOID_CHILDREN`    | content inside a void element (`<br>`, `<img>`, …)                |
| `ERR_VINCLE_DANGEROUS_HTML`   | `dangerouslySetInnerHTML.__html` is not a string                  |
| `ERR_VINCLE_FUNCTION_ATTR`    | a function passed as an attribute value                           |
| `ERR_VINCLE_VNODE_AS_TEXT`    | a component interpolated where text belongs                       |
| `ERR_VINCLE_SCOPE_COLLISION`  | overlapping renders or `Scope.with()` with no `AsyncLocalStorage` |
| `ERR_VINCLE_NO_STORE`         | the Scope store was read before `Scope.with()` ran                |
| `ERR_VINCLE_NO_SCOPE`         | a Scope call outside any `Scope.with()`                           |
| `ERR_VINCLE_CONTEXT_KEY`      | `Scope.key()` given something other than a non-empty string       |
| `ERR_VINCLE_CONTEXT_LIMIT`    | more distinct context keys than the table holds                   |
| `ERR_VINCLE_CONTEXT_UNSET`    | `Scope.get()` read a value never set in scope                     |
| `ERR_VINCLE_CONTEXT_CHILDREN` | a context `Consumer` whose children is not a function             |

The first three are `TypeError`s; every other code on this page is a plain
`Error`.

### `@vincle/flow`

| Code                                | Raised when                                          |
| ----------------------------------- | ---------------------------------------------------- |
| `ERR_VINCLE_FLOW_CONFIG`            | an option rejected at the entry point that took it   |
| `ERR_VINCLE_FLOW_NO_ADAPTER`        | a placeholder or fragment with no adapter configured |
| `ERR_VINCLE_FLOW_MERGE_UNSUPPORTED` | a `merge` the adapter cannot express                 |
| `ERR_VINCLE_FLOW_FRAGMENT_ID`       | a fragment id that cannot be a DOM id or URL segment |
| `ERR_VINCLE_FLOW_DUP_FRAGMENT`      | two `<Defer>` on the same `target` in one render     |
| `ERR_VINCLE_FLOW_NO_FRAGMENT`       | `renderFragment` produced nothing for that id        |
| `ERR_VINCLE_FLOW_NO_STREAMING`      | `renderToStream` given an adapter that cannot stream |

### `@vincle/vite-plugin` and `@vincle/precompile`

| Code                             | Raised when                                                |
| -------------------------------- | ---------------------------------------------------------- |
| `ERR_VINCLE_VITE_MANIFEST_READ`  | the Vite manifest could not be read                        |
| `ERR_VINCLE_VITE_MANIFEST_PARSE` | the manifest is not JSON                                   |
| `ERR_VINCLE_VITE_MANIFEST_SHAPE` | the file parsed but is not a manifest                      |
| `ERR_VINCLE_VITE_CONFIG`         | `setVite` given an option it cannot use                    |
| `ERR_VINCLE_VITE_MISSING_ENTRY`  | an entry the manifest does not list                        |
| `ERR_VINCLE_PRECOMPILE_CONFIG`   | `runtimeSource` is not a module specifier                  |
| `ERR_VINCLE_PRECOMPILE_HELPER`   | a runtime helper answered a shape the transform cannot use |
| `ERR_VINCLE_PRECOMPILE_INTERNAL` | an invariant of the transform broke: a bug to report       |

Configuration errors fail **fast**, at the entry point that receives the
option, before anything renders or a build starts. One exception is not an
error: an unreadable runtime module makes `@vincle/precompile` warn and fall
back to Deno's output.

## Per-fragment errors (streaming)

`@vincle/flow` takes `onError` on the render options and on each `<Defer>`:

```tsx
serve(req, () => <Page />, NativeAdapter, {
  onError: (error, { id, kind }) => {
    console.error(`Fragment ${id} failed:`, error);
    return <p>This section failed to load.</p>;
  },
});
```

See [renderToStream](/api/flow/renderToStream) for details.
