{"record":{"id":"c9ec410e9555631e","repo":"wavetermdev/waveterm","slug":"panic-in-s-w","errorCode":null,"errorMessage":"panic in %s: %w","messagePattern":"panic in (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"tsunami/util/util.go","lineNumber":32,"sourceCode":"\t\"strings\"\n\t\"time\"\n)\n\n// PanicHandler handles panic recovery and logging.\n// It can be called directly with recover() without checking for nil first.\n// Example usage:\n//\n//\tdefer func() {\n//\t    util.PanicHandler(\"operation name\", recover())\n//\t}()\nfunc PanicHandler(debugStr string, recoverVal any) error {\n\tif recoverVal == nil {\n\t\treturn nil\n\t}\n\tlog.Printf(\"[panic] in %s: %v\\n\", debugStr, recoverVal)\n\tdebug.PrintStack()\n\tif err, ok := recoverVal.(error); ok {\n\t\treturn fmt.Errorf(\"panic in %s: %w\", debugStr, err)\n\t}\n\treturn fmt.Errorf(\"panic in %s: %v\", debugStr, recoverVal)\n}\n\nfunc GetHomeDir() string {\n\thomeVar, err := os.UserHomeDir()\n\tif err != nil {\n\t\treturn \"/\"\n\t}\n\treturn homeVar\n}\n\nfunc ExpandHomeDir(pathStr string) (string, error) {\n\tif pathStr != \"~\" && !strings.HasPrefix(pathStr, \"~/\") && (!strings.HasPrefix(pathStr, `~\\`) || runtime.GOOS != \"windows\") {\n\t\treturn filepath.Clean(pathStr), nil\n\t}\n\thomeDir := GetHomeDir()\n\tif pathStr == \"~\" {","sourceCodeStart":14,"sourceCodeEnd":50,"githubUrl":"https://github.com/wavetermdev/waveterm/blob/a4447c1563b2df285ab89e76c82f91e1a1a49c1e/tsunami/util/util.go#L14-L50","documentation":"PanicHandler converts a recovered panic value into an error. When the recovered value is itself an error, it wraps it with %w so errors.Is/errors.As still work, prefixed with the debugStr describing where the panic occurred. The panic is also logged with a stack trace before returning.","triggerScenarios":"A deferred util.PanicHandler(\"name\", recover()) fires after the guarded code panicked with an error value — e.g. a nil-pointer dereference panic (runtime error implements error), or code that panicked with panic(err).","commonSituations":"RPC handler or goroutine panics on nil map/pointer access; library code panics with a sentinel error and the caller uses PanicHandler to convert it to a normal error return path.","solutions":["Fix the root cause: use errors.As/errors.Is on the returned error or read the log's stack trace to find the panicking line.","Add nil checks / bounds checks at the panic site (nil map init, nil pointer guard before dereference).","Keep the defer/recover pattern so the panic doesn't crash the process, and return the wrapped error to callers."],"exampleFix":"// before\nfunc run() {\n    m := map[string]int(nil)\n    _ = m[\"k\"] // read ok, but m[\"k\"] = 1 panics\n}\n// after\nfunc run() (err error) {\n    defer func() { err = util.PanicHandler(\"run\", recover()) }()\n    m := map[string]int{}\n    m[\"k\"] = 1\n    return nil\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"func isErrorPanic(v any) (error, bool) {\n    err, ok := v.(error)\n    return err, ok\n}","tryCatchPattern":"func run() (err error) {\n    defer func() { err = util.PanicHandler(\"run\", recover()) }()\n    // risky code\n    return nil\n}\n// caller:\nif err := run(); err != nil {\n    var target *MyErr\n    if errors.As(err, &target) { /* typed handling */ }\n}","preventionTips":["Always pair PanicHandler with defer func() { ... recover() } in goroutines and RPC entry points","Use errors.As/Is on the returned error since it wraps with %w","Fix the panicking site using the stack trace PanicHandler logs"],"tags":["panic","recovery","go"],"backgroundTag":"recovered-panic","analyzedSha":"a4447c1563b2df285ab89e76c82f91e1a1a49c1e","analyzedAt":"2026-09-01T15:26:23.972Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}