{"record":{"id":"b9e46e38dca46cb6","repo":"larksuite/cli","slug":"v","errorCode":null,"errorMessage":"%v","messagePattern":"%v","errorType":"exception","errorClass":"LifecycleError","httpStatus":null,"severity":"error","filePath":"internal/hook/emit.go","lineNumber":117,"sourceCode":"\t\t\t// Shutdown errors are logged, not propagated -- exit is\n\t\t\t// non-recoverable anyway.\n\t\t\tfmt.Fprintf(stderr(), \"warning: shutdown hook %q: %v\\n\", h.Name, err)\n\t\t}\n\t}\n\treturn nil\n}\n\n// callLifecycleSafe invokes a LifecycleHandler with panic recovery.\n// Returns *LifecycleError with Panic=true on recovered panic, Panic=false\n// on a regular returned error. nil if the handler succeeded.\nfunc callLifecycleSafe(ctx context.Context, h LifecycleEntry, lc *platform.LifecycleContext) (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = &LifecycleError{\n\t\t\t\tEvent:    lc.Event,\n\t\t\t\tHookName: h.Name,\n\t\t\t\tPanic:    true,\n\t\t\t\tCause:    fmt.Errorf(\"%v\", r),\n\t\t\t}\n\t\t}\n\t}()\n\tif e := h.Fn(ctx, lc); e != nil {\n\t\treturn &LifecycleError{\n\t\t\tEvent:    lc.Event,\n\t\t\tHookName: h.Name,\n\t\t\tPanic:    false,\n\t\t\tCause:    e,\n\t\t}\n\t}\n\treturn nil\n}\n","sourceCodeStart":99,"sourceCodeEnd":131,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/internal/hook/emit.go#L99-L131","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Read LifecycleError.Cause / HookName to identify which hook panicked, then fix the nil-dereference or bad state in that hook's code.","Make the hook defensive: check nil pointers/maps/slices and closed resources before use, and return an error instead of panicking.","Move one-shot initialization into Plugin.Install or the Plugin struct so per-hook state is never uninitialized.","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."],"exampleFix":"// before (hook that panics)\nfunc(ctx context.Context, lc *platform.LifecycleContext) error {\n    return state.conn.Close() // panics if state.conn is nil\n}\n// after\nfunc(ctx context.Context, lc *platform.LifecycleContext) error {\n    if state == nil || state.conn == nil {\n        return nil\n    }\n    return state.conn.Close()\n}","handlingStrategy":"try-catch","validationCode":"// before registering a hook, smoke-test it with a recovered call:\nfunc safeProbe(fn func(ctx context.Context) error) (err error) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\terr = fmt.Errorf(\"hook panics during probe: %v\", r)\n\t\t}\n\t}()\n\treturn fn(context.Background())\n}","typeGuard":"func isLifecyclePanic(err error) bool {\n\tvar le *hook.LifecycleError\n\treturn errors.As(err, &le) && le.Panic\n}","tryCatchPattern":"err := hookEmitter(ctx)\nvar le *hook.LifecycleError\nif errors.As(err, &le) && le.Panic {\n\tlog.Printf(\"hook %s panicked: %v\", le.HookName, le.Cause)\n\t// fall back to default shutdown/startup behavior\n}","preventionTips":["Never panic in hook code; return errors so they surface as Panic=false LifecycleErrors.","Nil-check all captured state (connections, maps, slices) at the top of every hook.","Initialize long-lived plugin state in Plugin.Install / on the Plugin struct, not lazily in hooks.","Watch stderr warnings during shutdown — lifecycle panic errors there are logged, not propagated."],"tags":["go","panic","hooks","lifecycle","plugins"],"backgroundTag":"panic-recovered-in-hook","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}