wavetermdev/waveterm · error
panic in %s: %w
Error message
panic in %s: %w
What it means
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.
Source
Thrown at tsunami/util/util.go:32
"strings"
"time"
)
// PanicHandler handles panic recovery and logging.
// It can be called directly with recover() without checking for nil first.
// Example usage:
//
// defer func() {
// util.PanicHandler("operation name", recover())
// }()
func PanicHandler(debugStr string, recoverVal any) error {
if recoverVal == nil {
return nil
}
log.Printf("[panic] in %s: %v\n", debugStr, recoverVal)
debug.PrintStack()
if err, ok := recoverVal.(error); ok {
return fmt.Errorf("panic in %s: %w", debugStr, err)
}
return fmt.Errorf("panic in %s: %v", debugStr, recoverVal)
}
func GetHomeDir() string {
homeVar, err := os.UserHomeDir()
if err != nil {
return "/"
}
return homeVar
}
func ExpandHomeDir(pathStr string) (string, error) {
if pathStr != "~" && !strings.HasPrefix(pathStr, "~/") && (!strings.HasPrefix(pathStr, `~\`) || runtime.GOOS != "windows") {
return filepath.Clean(pathStr), nil
}
homeDir := GetHomeDir()
if pathStr == "~" {View on GitHub (pinned to a4447c1563)
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.
Example fix
// before
func run() {
m := map[string]int(nil)
_ = m["k"] // read ok, but m["k"] = 1 panics
}
// after
func run() (err error) {
defer func() { err = util.PanicHandler("run", recover()) }()
m := map[string]int{}
m["k"] = 1
return nil
} Defensive patterns
Strategy: try-catch
Type guard
func isErrorPanic(v any) (error, bool) {
err, ok := v.(error)
return err, ok
} Try / catch
func run() (err error) {
defer func() { err = util.PanicHandler("run", recover()) }()
// risky code
return nil
}
// caller:
if err := run(); err != nil {
var target *MyErr
if errors.As(err, &target) { /* typed handling */ }
} Prevention
- 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
When it happens
Trigger: 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).
Common situations: 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.
Related errors
AI-assisted analysis of wavetermdev/waveterm@a4447c1563 (2026-09-01).
Data as JSON: /api/errors/c9ec410e9555631e.
Report an issue: GitHub.