siyuan-note/siyuan · error
failed to remove: %w
Error message
failed to remove: %w
What it means
Thrown by siyuan.storage.remove when os.RemoveAll fails to delete the resolved absolute path under the plugin's storage directory. The underlying OS error (permission denied, busy, not found) is wrapped with %w so it surfaces via the returned Promise rejection. It fires from a goroutine that recovers panics and routes errors to the JS reject handler.
Source
Thrown at kernel/plugin/api_storage.go:359
err = fmt.Errorf("panic during siyuan.storage.remove: %v", r)
}
p.worker.Run(func(rt *goja.Runtime) (_ any, _ error) {
if lo.IsNil(err) {
if resolveErr := resolve(result); resolveErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.storage.remove resolve: %v", p.Name, resolveErr)
}
} else {
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.storage.remove reject: %v", p.Name, rejectErr)
}
}
return
}, nil)
}()
if removeErr := os.RemoveAll(abs); removeErr != nil {
err = fmt.Errorf("failed to remove: %w", removeErr)
return
}
return
}()
return
}, func(rt *goja.Runtime, result any, err error) {
if !lo.IsNil(err) {
if rejectErr := reject(rt.NewGoError(err)); rejectErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.storage.remove reject: %v", p.Name, rejectErr)
}
}
})
if runErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.storage.remove worker run: %v", p.Name, runErr)
if rejectErr := reject(rt.NewGoError(runErr)); rejectErr != nil {
logging.LogErrorf("[plugin:%s] siyuan.storage.remove reject: %v", p.Name, rejectErr)
}View on GitHub (pinned to 251596fc0d)
Solutions
- Check the wrapped error (err.cause) to see the real OS error before deciding what to do.
- Ensure the SiYuan workspace/storage directory is writable by the kernel process and not locked by another app.
- Avoid concurrent removes of the same path; await the first remove before retrying.
- If the path may already be gone, catch the rejection and treat 'no such file or directory' as success.
Example fix
// before
await siyuan.storage.remove(p);
// after
try {
await siyuan.storage.remove(p);
} catch (e) {
if (!/no such file or directory/.test(String(e?.cause ?? e))) throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
if (typeof p !== 'string' || p.length === 0) throw new Error('remove: path required'); Type guard
function isRemovablePath(p: unknown): p is string { return typeof p === 'string' && p.length > 0 && !p.includes('..'); } Try / catch
try { await siyuan.storage.remove(p); } catch (e) { const cause = String((e as any)?.cause ?? e); if (/no such file/.test(cause)) return; throw e; } Prevention
- Always pass a non-empty relative path that resolves under the plugin storage dir.
- Await one remove before starting another on the same path.
- Treat ENOENT in the wrapped cause as benign when the goal is ensure-gone.
When it happens
Trigger: A plugin calls await siyuan.storage.remove(path) where path resolves under storageDir but the OS cannot delete it: file held open by another process, read-only filesystem, broken symlink, or the path was already removed concurrently.
Common situations: Running SiYuan with restrictive file permissions, antivirus/file-lock on Windows, a container with a read-only mounted storage volume, or a plugin that removes the same path from two concurrent calls.
Related errors
- failed to read directory: %w
- save OAuth client registration: %w
- save OAuth credentials: %w
- failed to add storage path to watcher: %v
- failed to make directory: %w
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/7998b10946f5bb1f.
Report an issue: GitHub.