{"id":"1c16e8e7da51eb4f","repo":"go-redis/redis","slug":"redis-autopipeline-panic-during-dispatch-v","errorCode":null,"errorMessage":"redis: autopipeline: panic during dispatch: %v","messagePattern":"redis: autopipeline: panic during dispatch: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"autopipeline.go","lineNumber":2180,"sourceCode":"\t}\n\treturn append(runs, cmds[start:])\n}\n\n// recoverDispatchPanic converts a panic on a dispatch goroutine (a hook or\n// command-encoder panic inside Process/Exec) into per-command errors instead\n// of crashing the process. On a plain client the same panic unwinds into the\n// CALLER, who can recover; the engine's dispatch goroutines have no caller,\n// so an unrecovered panic here would kill the whole program on behalf of one\n// bad command. Registered LAST at each dispatch site so it runs FIRST on\n// unwind (LIFO) — the errors are stamped before the deferred batch closes\n// wake the waiters. setCmdsErr fills only commands without an error, so\n// exec-recorded outcomes for commands that finished are preserved.\nfunc recoverDispatchPanic(cmds ...[]Cmder) {\n\tr := recover()\n\tif r == nil {\n\t\treturn\n\t}\n\terr := fmt.Errorf(\"redis: autopipeline: panic during dispatch: %v\", r)\n\tfor _, batch := range cmds {\n\t\tsetCmdsErr(batch, err)\n\t}\n\tinternal.Logger.Printf(context.Background(), \"autopipeline: recovered dispatch panic: %v\\n%s\", r, debug.Stack())\n}\n\n// flushBatchSlice takes the shard's currently-queued commands as one batch,\n// swaps in a fresh batch for subsequent enqueues, and dispatches the taken\n// batch. Completion is signalled by closing the batch's done channel once\n// (waking every waiter in a single operation) rather than one channel send\n// per command.\nfunc (s *apShard) flushBatchSlice() {\n\tap := s.ap\n\n\t// Drain every stripe into one combined batch and roll fresh queues for the\n\t// commands enqueued after this point. Striped enqueue spreads the hot\n\t// mutex; one merged flush keeps the pipeline deep. accumulateBatch already\n\t// bounds the total to roughly MaxBatchSize before we get here.","sourceCodeStart":2162,"sourceCodeEnd":2198,"githubUrl":"https://github.com/go-redis/redis/blob/36d97525cd8076aed67cddf54778e9ea84550929/autopipeline.go#L2162-L2198","documentation":"Produced when a panic occurs inside a dispatch goroutine of the AutoPipeliner (a user hook such as ProcessHook/DialHook, or a command encoder inside Process/Exec). Dispatch goroutines have no caller frame to recover the panic, so an unrecovered panic would crash the whole process on behalf of one bad command. recoverDispatchPanic (autopipeline.go:2175) catches it, stamps the error on every command in the affected batch(es) via setCmdsErr, and logs the stack via internal.Logger so the program keeps running.","triggerScenarios":"Registering a ProcessHook that dereferences a nil pointer or indexes out of range; a custom command-encoder path that panics; a hook that closes over a resource freed concurrently. The panic is recovered on the flusher/dispatch goroutine and surfaces as cmd.Err() on the commands in that batch.","commonSituations":"Buggy instrumentation hook (OTel/metrics); a hook that assumes a non-nil context value the caller forgot to set; a hook upgraded across a version that changed the Cmder shape; concurrent use of a shared unsafe resource from a hook.","solutions":["Inspect the stack trace logged by internal.Logger (set redis.SetLogger to capture it) to find the panicking frame.","Make the hook defensive: recover inside the hook itself, or guard nil/length assumptions before use.","Check every cmd.Err() in the batch (not just one) — setCmdsErr stamps the same error on all commands that had no error yet.","Reproduce with a minimal hook in a test to confirm the fix."],"exampleFix":"// before — hook panics on nil tag\nrdb.AddHook(redis.ProcessHook(func(ctx context.Context, cmd redis.Cmder, next redis.ProcessHook) error {\n    tag := ctx.Value(\"tag\").(string) // panic if tag missing\n    fmt.Println(tag)\n    return next(ctx, cmd)\n}))\n\n// after — guard the assertion\nrdb.AddHook(redis.ProcessHook(func(ctx context.Context, cmd redis.Cmder, next redis.ProcessHook) error {\n    if v, ok := ctx.Value(\"tag\").(string); ok {\n        fmt.Println(v)\n    }\n    return next(ctx, cmd)\n}))","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"// Every command in the batch may carry the recovered-panic error,\n// not just the first. Iterate all commands.\nfor _, c := range cmds {\n    if err := c.Err(); err != nil && strings.Contains(err.Error(), \"panic during dispatch\") {\n        // surface to monitoring; the panic stack was logged via internal.Logger\n    }\n}","preventionTips":["Set redis.SetLogger so recovered-panic stacks are captured.","Recover inside your own hooks so a bug does not poison a whole batch.","Guard nil/length assumptions in hooks; never assume context values are present.","Write a unit test per hook that exercises it in isolation."],"tags":["autopipeline","panic-recovery","hooks","concurrency","debugging"],"analyzedSha":"36d97525cd8076aed67cddf54778e9ea84550929","analyzedAt":"2026-08-06T01:08:27.376Z","schemaVersion":2}