{"record":{"id":"40258aec7ab44ea9","repo":"etcd-io/etcd","slug":"unexpected-revision-0-calling-syncupdates-befor","errorCode":null,"errorMessage":"unexpected revision = 0. Calling SyncUpdates before SyncBase finishes?","messagePattern":"unexpected revision = 0\\. Calling SyncUpdates before SyncBase finishes\\?","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/mirror/syncer.go","lineNumber":117,"sourceCode":"\t\t\t\treturn\n\t\t\t}\n\n\t\t\trespchan <- resp\n\n\t\t\tif !resp.More {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// move to next key\n\t\t\tkey = string(append(resp.Kvs[len(resp.Kvs)-1].Key, 0))\n\t\t}\n\t}()\n\n\treturn respchan, errchan\n}\n\nfunc (s *syncer) SyncUpdates(ctx context.Context) clientv3.WatchChan {\n\tif s.rev == 0 {\n\t\tpanic(\"unexpected revision = 0. Calling SyncUpdates before SyncBase finishes?\")\n\t}\n\treturn s.c.Watch(ctx, s.prefix, clientv3.WithPrefix(), clientv3.WithRev(s.rev+1))\n}\n","sourceCodeStart":99,"sourceCodeEnd":121,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/mirror/syncer.go#L99-L121","documentation":"mirror.NewSyncer(...).SyncBase(ctx) returns channels; the syncer records the latest revision it saw as s.rev. SyncUpdates(ctx) starts a watch from s.rev+1 and panics if s.rev is still 0, i.e. when no revision has been captured yet. The panic message itself asks the diagnostic question: SyncUpdates was called before SyncBase had delivered (and the syncer processed) its first response, so there is no valid revision to resume from.","triggerScenarios":"Calling s.SyncUpdates(ctx) immediately after s.SyncBase(ctx) without waiting for any kv or header to arrive on the returned channels; SyncBase whose context is cancelled before the first range completes; an empty keyspace is fine (the header still carries a revision) but a failed/interrupted initial sync leaves rev==0.","commonSituations":"Race at service startup: goroutine consuming SyncUpdates starts before the SyncBase goroutine receives the first response; cancelling SyncBase's context on shutdown and then (re)starting SyncUpdates on the same syncer; reusing a syncer after an error instead of creating a new one.","solutions":["Wait until SyncBase has delivered at least one response (or completed) before calling SyncUpdates — read from the SyncBase channels in the same goroutine that then calls SyncUpdates.","Run the full mirror pattern in one goroutine: consume baseChan/errChan to exhaustion, then start SyncUpdates (see the documented Mirror example in clientv3/mirror).","If SyncBase's context was cancelled or errored, create a fresh syncer via mirror.NewSyncer instead of continuing with the spent one.","As a structural guard, wrap SyncUpdates invocation with a check that SyncBase finished (e.g. signal channel closed in your consumer)."],"exampleFix":"// before\nbaseChan, errChan := syncer.SyncBase(ctx)\ngo func() { for range baseChan {} }() // not drained / racing\nupChan := syncer.SyncUpdates(ctx) // panics: unexpected revision = 0\n\n// after — mirror pattern: drain SyncBase fully, then SyncUpdates\nbaseChan, errChan := syncer.SyncBase(ctx)\nfor resp := range baseChan {\n    handle(resp) // syncer records rev from these responses\n}\nfor err := range errChan {\n    if err != nil { return err }\n}\nupChan := syncer.SyncUpdates(ctx) // safe: rev > 0","handlingStrategy":"validation","validationCode":"// structural guard: only start SyncUpdates after SyncBase channels are exhausted\nbaseCh, errCh := syncer.SyncBase(ctx)\nfor r := range baseCh { handle(r) }\nfor e := range errCh {\n    if e != nil { return e }\n}\n// safe point: syncer.rev > 0\nupCh := syncer.SyncUpdates(ctx)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Consume SyncBase to completion (both channels) in the same goroutine before calling SyncUpdates — the documented mirror pattern.","Never reuse a syncer whose SyncBase context was cancelled; build a fresh mirror.NewSyncer.","Treat 'SyncUpdates panics' as a startup-ordering bug in your code, not an etcd failure."],"tags":["go","etcd","clientv3","mirror","watch","panic","ordering","startup-race"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}