{"record":{"id":"ce9c6093de5b3893","repo":"perspective-dev/perspective","slug":"websocket-message-dropped-ws-readystate","errorCode":null,"errorMessage":"WebSocket message dropped (${ws.readyState})","messagePattern":"WebSocket message dropped \\((.+?)\\)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"rust/perspective-js/src/ts/websocket.ts","lineNumber":80,"sourceCode":"\n        ws.onmessage = (event) => {\n            client.handle_response(event.data);\n        };\n\n        await receiver;\n    }\n\n    async function send_message(proto: Uint8Array) {\n        if (\n            ws.readyState === WebSocket.CLOSING ||\n            ws.readyState === WebSocket.CLOSED\n        ) {\n            const msg = `WebSocket transport error (${ws.readyState})`;\n            client.handle_error(msg, connect);\n            throw new Error(msg);\n        } else if (ws.readyState === WebSocket.CONNECTING) {\n            const msg = `WebSocket message dropped (${ws.readyState})`;\n            throw new Error(msg);\n        } else {\n            const buffer = proto.slice().buffer;\n            ws.send(buffer);\n        }\n    }\n\n    async function on_close() {\n        console.debug(\"Closing WebSocket\");\n        ws.close();\n    }\n\n    client = new Client(send_message, on_close);\n    await connect();\n    return client;\n}\n","sourceCodeStart":62,"sourceCodeEnd":96,"githubUrl":"https://github.com/perspective-dev/perspective/blob/11c8238c0c9c0b9e490b5a6a2dc277afad1e980a/rust/perspective-js/src/ts/websocket.ts#L62-L96","documentation":"The perspective WebSocket client throws this when `send_message` is called while the socket is still CONNECTING (readyState 0) — the connection has not finished opening, so the outgoing message is dropped rather than sent. Unlike the transport error, `handle_error`/reconnect is not invoked; the message is simply discarded and the error thrown to the caller.","triggerScenarios":"Calling `send_message` (directly or via a higher-level API) between initiating the WebSocket connection and receiving its `open` event — e.g. sending immediately after constructing the client without awaiting connection, or racing requests against a reconnect in progress.","commonSituations":"Not awaiting the promise/`open` event from `websocket()` before issuing table/view commands; firing requests in a framework mount lifecycle before the async connection resolves; reconnect logic that recreates the socket while pending calls still target the new CONNECTING socket.","solutions":["Await the client's connection promise (or the WebSocket `open` event) before calling send_message or any API that sends.","Gate sends on readyState: wait/poll until `ws.readyState === WebSocket.OPEN`.","Serialize sends behind the connection lifecycle: queue messages raised while CONNECTING and flush them on `open`.","In reconnect logic, ensure in-flight/pending requests are re-attached to the newly opened socket instead of the old one."],"exampleFix":"// before\nconst client = websocket(\"ws://host/perspective\");\nclient.send_message(openTableReq); // socket still CONNECTING -> dropped\n\n// after\nconst client = await websocket(\"ws://host/perspective\"); // await connection\n// or: await new Promise(r => client.ws.addEventListener(\"open\", r));\nclient.send_message(openTableReq);","handlingStrategy":"retry","validationCode":"async function waitForOpen(ws, timeoutMs = 10000) {\n  if (ws.readyState === WebSocket.OPEN) return;\n  await new Promise((resolve, reject) => {\n    const t = setTimeout(() => reject(new Error(\"connect timeout\")), timeoutMs);\n    ws.addEventListener(\"open\", () => { clearTimeout(t); resolve(); }, { once: true });\n    ws.addEventListener(\"error\", () => { clearTimeout(t); reject(new Error(\"connect failed\")); }, { once: true });\n  });\n}","typeGuard":"function isOpen(ws: WebSocket): boolean {\n  return ws.readyState === WebSocket.OPEN;\n}","tryCatchPattern":"try {\n  client.send_message(msg);\n} catch (e) {\n  if (/^WebSocket message dropped \\(0\\)/.test(e.message)) {\n    await waitForOpen(client.ws);\n    client.send_message(msg); // retry once open\n  } else throw e;\n}","preventionTips":["Never fire requests before awaiting the client's connection promise or the `open` event.","Queue messages emitted during CONNECTING and flush them on `open`.","Initialize the perspective client once at app startup and share the awaited instance.","In reconnect flows, route all senders through a single gate that waits for OPEN."],"tags":["websocket","async","race-condition","connection-state"],"backgroundTag":"invalid-state-transition","analyzedSha":"11c8238c0c9c0b9e490b5a6a2dc277afad1e980a","analyzedAt":"2026-09-09T00:35:19.377Z","contentChangedAt":"2026-09-09T00:35:19.377Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}