siyuan-note/siyuan · error
panic during lifecycle hook invocation: %v
Error message
panic during lifecycle hook invocation: %v
What it means
invokeHook recovers a panic thrown while calling a plugin lifecycle hook (onload, onunload, etc.) inside the goja runtime. The panic value is wrapped with this message and logged via logging.LogErrorf as "lifecycle hook [%q] error"; it is not propagated, so the plugin process survives but the hook did not complete.
Source
Thrown at kernel/plugin/plugin.go:850
p.watcherMu.Unlock()
close(done)
}
func storageWatchOperations(event fsnotify.Event) (ret []string) {
for _, operation := range []fsnotify.Op{fsnotify.Create, fsnotify.Write, fsnotify.Rename, fsnotify.Remove} {
if event.Has(operation) {
ret = append(ret, operation.String())
}
}
return
}
// invokeHook calls a lifecycle hook (e.g. onload) if it exists, awaiting if it returns a Promise.
func (p *KernelPlugin) invokeHook(name string) {
var err error
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic during lifecycle hook invocation: %v", r)
}
if err != nil {
logging.LogErrorf("[plugin:%s] lifecycle hook [%q] error: %v", p.Name, name, err)
}
}()
done := make(chan TaskResult, 1)
runErr := p.worker.Run(func(rt *goja.Runtime) (_ any, err error) {
lifecycle, err := getJsContextValue(rt, []any{"siyuan", "plugin", "lifecycle"})
if err != nil {
return
}
if lifecycle == nil {
err = fmt.Errorf("globalThis.siyuan.plugin.lifecycle not found")
return
}View on GitHub (pinned to 8641553a1f)
Solutions
- Read the log line `lifecycle hook [%q] error: %v` to see the panic value and hook name
- Fix the plugin's hook implementation - typically a nil dereference or call into an already-closed runtime
- Wrap risky sections of the plugin's hook in try/catch (JS) so panics do not escape into goja
- Check kernel-side nil guards in objects passed to the hook before invocation
Example fix
// plugin JS (before)
onload() { this.data.items.forEach(...) } // this.data undefined -> panic
// after
onload() {
if (!this.data || !Array.isArray(this.data.items)) return
this.data.items.forEach(...)
} Defensive patterns
Strategy: try-catch
Validate before calling
// plugin JS: pre-flight check before kernel invokes the hook
if (typeof this.onload !== 'function' || typeof this.onunload !== 'function') {
throw new Error('lifecycle hooks missing')
} Try / catch
p.invokeHook("onload") // panics are recovered inside invokeHook
// caller side: treat log line `lifecycle hook ["onload"] error: ...` as plugin fault,
// optionally disable the plugin after repeated hook failures Prevention
- Wrap plugin hook bodies in try/catch on the JS side
- Null-check all kernel-provided objects before use inside hooks
- Avoid touching freed resources (sockets, runtime) in onunload
- Test plugin load/unload cycles repeatedly to surface teardown races
When it happens
Trigger: A lifecycle hook (or code it calls synchronously) panics inside the goja runtime - e.g. calling a method on a nil kernel-provided object, a runtime.GoRundown during shutdown, or a JS callback that re-enters the runtime.
Common situations: Plugin onload accessing kernel APIs before initialization; onunload touching already-freed sockets; hooks that throw during plugin reload/disable cycles.
Related errors
- injectServer: %v
- injectStorage: %v
- panic during plugin stop: %v
- globalThis.siyuan.plugin.lifecycle not found
- globalThis.siyuan.plugin.lifecycle is not an object
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/b89b8895d81eb0a4.
Report an issue: GitHub.