Troubleshooting
What each thrown error means, how a sync connection ends and why, and the refused status a command returns instead of throwing.
Edytor reports problems three ways: it throws when you misuse an API, a provider reports how a connection ended through events and the document, and a command that cannot apply returns a refused status instead of throwing. This page lists each with its cause and fix.
Thrown errors
| Error | Thrown by | Cause and fix |
|---|---|---|
DocumentNotReadyError |
document.history |
The document is still pending: a provider may still bring content. Wait for onReady, or call document.sync(). |
DocumentDestroyedError |
any document member, attachSync |
The document was destroyed. A view destroys only the document it created itself. |
SemanticConflictError |
createDocument, a view mounting |
Two sources declare different block rules (role, default child, default type) for one kind: two views with different plugins, two plugins in one view, or a headless semantics option a view’s plugins contradict. Give every view of a document the same plugins. |
UndecodableUpdateError |
loadDocument |
The bytes are not an update. Your bytes are never modified. |
UnsupportedDocError |
loadDocument |
Not an edytor document; with reason legacy, a v13 (yjs) document: migrate it. |
SchemaMismatchError |
loadDocument |
The content claims a schema this build cannot own (a newer build wrote it). |
GenerationMismatchError |
migrate; reported, not thrown, by an IndexedDB store (the provider’s failed) and by the room (its failure) |
A store of another schema generation. The v14 development generations 1–3 are not migrated: re-import them from JSON. For a room, reset() drops the stored container and starts again from onLoad (generation cutover). |
TypeError: room id "…" cannot be dialed |
WebsocketProvider, createWebsocketSync |
The room id is empty, ., .., over 256 characters, or holds a lone surrogate. A URL collapses a . or .. segment, and cannot encode a lone surrogate, so no dial could reach that room. Pick another id. (<Edytor server room> does not throw: it reports the 4400 refusal below.) |
EdytorDocDisposedError |
the facade | The facade was used after its document was destroyed. |
Error: [edytor-doc] stale plan |
facade.apply(plan) |
The plan was prepared before another write. Prepare and apply in the same turn, or compose plans prepared at one version. |
Error: No Edytor found |
useEdytor() |
Called outside a component rendered inside <Edytor>. |
Error: EdytorOptions: document cannot be combined… |
new Edytor(…) |
Pass either document or doc/awareness/actor, not both. |
Documents lists the document errors with their options.
A block kind no plugin registers does not throw: it renders as a plain block (its text and children) and logs one warning per kind in development. A chord such as cmd+s does not throw either: it fails type-checking and warns in development (chord syntax).
Sync connections
The WebSocket provider reports how a connection ended through its events, and the document keeps a terminal refusal as document.syncRefusal / onSyncRefused. What the client sees depends on the close code:
| Close | Means | The client |
|---|---|---|
1006, or no connection |
The server is unreachable, or refused the upgrade with an HTTP error, which a browser cannot tell from a network failure (a Worker route that does not match the room path, say: the provider sends the room id as one percent-encoded path segment). | Keeps redialing with a growing backoff (up to 30 s) and emits unreachable with { attempts, nextRetryMs }. params are re-read at each dial, so a refreshed token gets in. If your Worker turns the dial away itself (an unknown document, an origin it does not serve), answer with closedSocket and a 4xxx code, so the client stops. |
4400 invalid document id |
The room name is empty, ., .., longer than 256 characters or holds a lone surrogate (routeDocumentSocket never calls authorize). The provider refuses such a name itself, with a TypeError at construction; <Edytor server room> reports it as this refusal through onSyncRefused without dialing. |
Terminal, like 4403. |
4401 expired |
Your authorize returned { expired: true }: the credential expired. |
Not a refusal: emits expired and redials after the backoff, reading params again. Put a fresh token in params when expired fires (onSyncExpired on <Edytor>). |
4403 document access denied |
Your authorize returned null for this user and document, or an invalid identity: a userId that is empty or not a string, over 256 characters or with a lone surrogate (a name cut mid-emoji), or a replica that is not a client id. |
Terminal: emits refused and stops dialing. An empty document stays pending. Fix the permission, then reload. |
4409 replica bound to another user |
The client dialed with a client id another user owns (a browser profile shared by two users). | Terminal, like 4403. See replicas. |
1008 refused: <reason> |
The room refused this client for good; the reason is one of generation (a build of another schema generation), replica (a presence entry for a client id another user owns), schema (a foreign schema stamp), identity, malformed (bytes the room cannot decode), or container (the room’s stored container is of another generation, torn, or its onLoad returned content it cannot use). Content an update adds under another user’s client id is not a refusal: it is stripped and the socket stays. |
Terminal, like 4403. Every socket of a room with a container refusal is refused until an operator fixes the storage (for a generation mismatch, reset()). |
Any other 4xxx |
An application refusal from your own server. | Terminal, like 4403. |
1011 storage failure |
The room could not store an update or a client-id registration. | Redials; the handshake resends the edit. While the fault repeats, each 1011 doubles the backoff and emits unreachable, until the room acknowledges the edits. |
1011 internal error |
The room’s CRDT engine or a send failed; the room rebuilt its document from storage. | Redials, backing off the same way; the handshake resends the edit. |
1011 room unavailable |
The room’s onLoad threw, or the room could not read its rows (when it started, or after a storage failure). |
Redials; the room loads again at the next dial. |
permission-denied message (read-only) |
The read-only socket sent an edit (joining alone sends a notice that sets provider.readOnly, not this event). |
The socket stays open and syncs; the edit is not stored. Render read-only users with a read-only view. |
Before a document has content, a terminal refusal never seeds its value: the document stays pending and document.syncRefusal holds the SyncRefusedError (code, reason). A document that already has content keeps it and stays editable offline.
On the server, room.refusals keeps the newest 100 refusals with their detail and refusalCounts counts them by reason; see diagnostics.
Other provider signals:
schema-mismatch: an update carrying a foreign schema stamp was dropped; the connection stays.protocol-mismatch: a frame of another generation arrived and was dropped.document.writable === false: the document holds content from a schema this build cannot own. Every write is refused and providers stop sending until it heals.unsavednever reaches0: the server sends no store acknowledgements (a relay that is not the edytor room). Or it stays above0after deleting text a restore lost: the room holds its limit of waiting deletes and dropped this one (awaitingentry in the room’srefusals); the next connection resends it (see the room’s storage). If the limit stays full,dropWaitingDeletes()on the room empties it (a class that usesattachDocumentforwards it as its own method: RPC does not reach the returned document).
Refused commands
Editing commands never throw because they cannot apply. They report a status:
edytor.dispatcher.last.statusafter a view command:applied,noop(it ran and changed nothing),refused(nothing was written and no undo step recorded) orfailed(a hook or the command threw; the error is rethrown and kept inlast.error).- An
OpResultfrom a facade operation:applied,nooporrefused, with areasonwhen the document names one (id-collision).
A command is refused when:
- the view is
readonly, or the document is notwritable; - a plugin’s
onBeforeOperationcalledprevent()on the command or on one of its steps; - the document forbids it: a void block takes no children and never merges or splits, an island’s blocks never leave it and nothing moves into it, a block would sit directly in a list it is no item of (a move or outdent of an image there; see containers), a target is missing or deleted, or an id already exists.
Check last.status after a command you issue when the next step depends on it:
block.setData({ ...block.data, checked: true });
if (edytor.dispatcher.last?.status === 'refused') showReadonlyNotice();
Installing the pre-release
| Symptom | Cause | Fix |
|---|---|---|
ERR_PNPM_TARBALL_INTEGRITY or EINTEGRITY on an install of the hosted tarball |
Your lockfile pins the hash of other bytes than the ones served at that URL. The site never replaces a tarball under the same version (a new build gets a new pre-release version and URL), so this means the lockfile entry came from elsewhere, or from a forced redeploy. | Remove the package, then install the URL again, which records the served hash: pnpm remove edytor && pnpm add <that URL> (npm: npm uninstall edytor && npm install <that URL>). With pnpm, installing the same URL without removing it first keeps the pinned hash and fails the same way. Commit the lockfile. To move to a newer pre-release, install its URL from Install. |
ERR_PNPM_FETCH_404 (or npm’s 404 Not Found) on a hosted tarball URL |
The site never hosted that version: a typo in the URL, or a version from a local build. Every pre-release it has served keeps answering. | Install the URL from Install. |
| After an upgrade, a room opens empty while clients not yet upgraded keep editing it | Your Worker routes on the raw path segment. Since 0.1.0-next.1 the provider percent-encodes the room id (a:b dials …/rooms/a%3Ab), so an id holding $, %, &, +, ,, :, ;, =, @, [, ] or | names another Durable Object: an older client sent those characters unescaped (50%off against 50%25off; Chromium already escaped |). (An id holding /, \, ?, #, a tab or a line break never reached its own room from an older client at all.) |
Decode the segment as the quick start does, and deploy the Worker with the upgraded clients; see upgrading between pre-releases. |