larksuite/cli · error · LifecycleError

%v

Error message

%v

What it means

callLifecycleSafe in internal/hook/emit.go recovers panics thrown by a lifecycle hook handler and converts them into a *LifecycleError with Panic=true, storing the panic value formatted with fmt.Errorf("%v", r) as Cause. The message "%v" means the visible text is whatever the panic value stringifies to — it may not be an error at all (string, runtime error, nil-map write, etc.). Lifecycle hooks are user/plugin-supplied functions, so the CLI isolates a crash in one hook instead of crashing the process.

Source

Thrown at internal/hook/emit.go:117

			// Shutdown errors are logged, not propagated -- exit is
			// non-recoverable anyway.
			fmt.Fprintf(stderr(), "warning: shutdown hook %q: %v\n", h.Name, err)
		}
	}
	return nil
}

// callLifecycleSafe invokes a LifecycleHandler with panic recovery.
// Returns *LifecycleError with Panic=true on recovered panic, Panic=false
// on a regular returned error. nil if the handler succeeded.
func callLifecycleSafe(ctx context.Context, h LifecycleEntry, lc *platform.LifecycleContext) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = &LifecycleError{
				Event:    lc.Event,
				HookName: h.Name,
				Panic:    true,
				Cause:    fmt.Errorf("%v", r),
			}
		}
	}()
	if e := h.Fn(ctx, lc); e != nil {
		return &LifecycleError{
			Event:    lc.Event,
			HookName: h.Name,
			Panic:    false,
			Cause:    e,
		}
	}
	return nil
}

View on GitHub (pinned to 7fd6ef3c07)

Solutions

  1. Read LifecycleError.Cause / HookName to identify which hook panicked, then fix the nil-dereference or bad state in that hook's code.
  2. Make the hook defensive: check nil pointers/maps/slices and closed resources before use, and return an error instead of panicking.
  3. Move one-shot initialization into Plugin.Install or the Plugin struct so per-hook state is never uninitialized.
  4. If the hook is third-party, update or remove the plugin; note that on the shutdown path the error is only logged as a warning, so check stderr output.

Example fix

// before (hook that panics)
func(ctx context.Context, lc *platform.LifecycleContext) error {
    return state.conn.Close() // panics if state.conn is nil
}
// after
func(ctx context.Context, lc *platform.LifecycleContext) error {
    if state == nil || state.conn == nil {
        return nil
    }
    return state.conn.Close()
}
Defensive patterns

Strategy: try-catch

Validate before calling

// before registering a hook, smoke-test it with a recovered call:
func safeProbe(fn func(ctx context.Context) error) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("hook panics during probe: %v", r)
		}
	}()
	return fn(context.Background())
}

Type guard

func isLifecyclePanic(err error) bool {
	var le *hook.LifecycleError
	return errors.As(err, &le) && le.Panic
}

Try / catch

err := hookEmitter(ctx)
var le *hook.LifecycleError
if errors.As(err, &le) && le.Panic {
	log.Printf("hook %s panicked: %v", le.HookName, le.Cause)
	// fall back to default shutdown/startup behavior
}

Prevention

When it happens

Trigger: Any LifecycleHandler.Fn (shutdown/startup lifecycle hook, run via emitLifecycle/lifecycle emission) panics: indexing a nil slice, calling methods on a nil pointer, closing a nil channel, or panic("...") inside plugin hook code invoked with (ctx, *platform.LifecycleContext).

Common situations: A plugin's shutdown hook dereferences state that was never initialized; a hook's captured resource was already released by an earlier hook; Go runtime errors like "index out of range" surfacing as the cause string.

Related errors


AI-assisted analysis of larksuite/cli@7fd6ef3c07 (2026-09-04). Data as JSON: /api/errors/b9e46e38dca46cb6. Report an issue: GitHub.