{"record":{"id":"e4a5d5ebac65b7ac","repo":"wavetermdev/waveterm","slug":"client-is-done","errorCode":null,"errorMessage":"client is done","messagePattern":"client is done","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/waveapp/waveapp.go","lineNumber":234,"sourceCode":"\t}\n\tif !gotRoute {\n\t\treturn fmt.Errorf(\"vdom context route could not be established\")\n\t}\n\twshclient.EventSubCommand(c.RpcClient, wps.SubscriptionRequest{Event: wps.Event_BlockClose, Scopes: []string{\n\t\tblockORef.String(),\n\t}}, nil)\n\tc.RpcClient.EventListener.On(\"blockclose\", func(event *wps.WaveEvent) {\n\t\tc.doShutdown(\"got blockclose event\")\n\t})\n\treturn nil\n}\n\nfunc (c *Client) SendAsyncInitiation() error {\n\tif c.VDomContextBlockId == \"\" {\n\t\treturn fmt.Errorf(\"no vdom context block id\")\n\t}\n\tif c.GetIsDone() {\n\t\treturn fmt.Errorf(\"client is done\")\n\t}\n\treturn wshclient.VDomAsyncInitiationCommand(\n\t\tc.RpcClient,\n\t\tvdom.MakeAsyncInitiationRequest(c.RpcContext.BlockId),\n\t\t&wshrpc.RpcOpts{Route: wshutil.MakeFeBlockRouteId(c.VDomContextBlockId)},\n\t)\n}\n\nfunc (c *Client) SetAtomVals(m map[string]any) {\n\tfor k, v := range m {\n\t\tc.Root.SetAtomVal(k, v, true)\n\t}\n}\n\nfunc (c *Client) SetAtomVal(name string, val any) {\n\tc.Root.SetAtomVal(name, val, true)\n}\n","sourceCodeStart":216,"sourceCodeEnd":252,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/pkg/waveapp/waveapp.go#L216-L252","documentation":"SendAsyncInitiation checks c.GetIsDone() and refuses to send if the client has already shut down (blockclose event, doShutdown, or explicit close). Once done, the RPC connection is being torn down, so initiating async vdom rendering would target a dead connection. This is a post-shutdown state guard.","triggerScenarios":"Calling SendAsyncInitiation after the 'blockclose' event fired doShutdown, after the user closed the Wave block, or after an explicit shutdown/close call on the client.","commonSituations":"Background goroutines or timers still firing after block close; attempting to re-render on a client whose block was closed by the user; reusing a Client instance across block sessions.","solutions":["Check client.GetIsDone() before calling SendAsyncInitiation and skip gracefully if done.","Stop/tear down goroutines and timers in your shutdown handler (subscribe to the 'blockclose' event).","Treat this error as a benign no-op in background workers rather than retrying.","Create a new Client via Connect for a new block session instead of reusing the done client.","Register cleanup in the On(\"blockclose\") handler to cancel pending sends."],"exampleFix":"// before\nif err := client.SendAsyncInitiation(); err != nil { panic(err) }\n// after\nif client.GetIsDone() { return } // block closed; nothing to do\nif err := client.SendAsyncInitiation(); err != nil { log.Printf(\"async initiation skipped: %v\", err) }","handlingStrategy":"type-guard","validationCode":"if client.GetIsDone() {\n    return nil // block closed; skip silently\n}","typeGuard":"func canSend(c *waveapp.Client) bool {\n    return c.VDomContextBlockId != \"\" && !c.GetIsDone()\n}","tryCatchPattern":"if err := client.SendAsyncInitiation(); err != nil {\n    if strings.Contains(err.Error(), \"client is done\") {\n        return nil // benign: block already closed\n    }\n    return err\n}","preventionTips":["Check GetIsDone() before any RPC after shutdown is possible.","Cancel background goroutines/timers in the On(\"blockclose\") handler.","Never reuse a Client across block sessions; create a new one via Connect.","Treat this error as a no-op signal, not a failure to retry."],"tags":["lifecycle","shutdown","vdom","state"],"backgroundTag":"client-already-closed","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}