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

  1. Check the wrapped error (err.cause) to see the real OS error before deciding what to do.
  2. Ensure the SiYuan workspace/storage directory is writable by the kernel process and not locked by another app.
  3. Avoid concurrent removes of the same path; await the first remove before retrying.
  4. 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

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


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/7998b10946f5bb1f. Report an issue: GitHub.