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

  1. 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.
  2. Add nil checks / bounds checks at the panic site (nil map init, nil pointer guard before dereference).
  3. 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

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.