jamiepine/voicebox · error · Error

Failed to resolve save path from dialog

Error message

Failed to resolve save path from dialog

What it means

Thrown by the Tauri saveFile() platform implementation after the native save dialog resolves. The @tauri-apps/plugin-dialog save() can return a string path OR an object of shape { path: string } depending on dialog version/platform; the code handles both, then throws only if neither yields a usable string. This is a defensive guard for an unexpected return shape, not a normal user-cancel path (cancel returns null/falsy and is handled earlier).

Source

Thrown at tauri/src/platform/filesystem.ts:19

import type { FileFilter, PlatformFilesystem } from '@/platform/types';

export const tauriFilesystem: PlatformFilesystem = {
  async saveFile(filename: string, blob: Blob, filters?: FileFilter[]) {
    const { save } = await import('@tauri-apps/plugin-dialog');
    const { writeFile } = await import('@tauri-apps/plugin-fs');

    const filePath = await save({
      defaultPath: filename,
      filters: filters || [],
    });

    if (!filePath) return; // User cancelled the dialog

    const resolvedPath =
      typeof filePath === 'string' ? filePath : (filePath as { path: string }).path;

    if (!resolvedPath) {
      throw new Error('Failed to resolve save path from dialog');
    }

    const arrayBuffer = await blob.arrayBuffer();
    await writeFile(resolvedPath, new Uint8Array(arrayBuffer));
  },

  async openPath(path: string) {
    const { open } = await import('@tauri-apps/plugin-shell');
    await open(path);
  },

  async pickDirectory(title: string) {
    const { open } = await import('@tauri-apps/plugin-dialog');
    const selected = await open({ directory: true, title });
    if (!selected) return null;
    const dir = typeof selected === 'string' ? selected : (selected as { path: string }).path;
    return dir || null;
  },

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Confirm @tauri-apps/plugin-dialog version matches the documented return shape (string on Tauri v1, { path } on some v2 paths).
  2. Log the raw resolved value when this throws to learn the actual shape: console.error('save returned', filePath).
  3. Broaden the normalization to also read filePath / name / path fields before giving up.
  4. Ensure the Tauri capability for dialog:allow-save is granted in capability files.

Example fix

// before
const resolvedPath =
  typeof filePath === 'string' ? filePath : (filePath as { path: string }).path;
if (!resolvedPath) {
  throw new Error('Failed to resolve save path from dialog');
}
// after
const resolvedPath =
  typeof filePath === 'string'
    ? filePath
    : (filePath as { path?: string; filePath?: string }).path ??
      (filePath as { filePath?: string }).filePath;
if (!resolvedPath || typeof resolvedPath !== 'string') {
  throw new Error(`Failed to resolve save path from dialog: ${JSON.stringify(filePath)}`);
}
Defensive patterns

Strategy: type-guard

Type guard

type SaveDialogResult = string | { path: string } | { filePath: string } | null;
function resolveSavePath(v: SaveDialogResult): string | null {
  if (typeof v === 'string') return v;
  if (v && typeof v === 'object') return v.path ?? v.filePath ?? null;
  return null;
}

Try / catch

let resolvedPath: string | null = null;
try {
  const filePath = await save({ defaultPath: filename, filters: filters || [] });
  resolvedPath = resolveSavePath(filePath as SaveDialogResult);
} catch (e) {
  throw new Error(`Save dialog failed: ${(e as Error).message}`);
}
if (!resolvedPath) return; // user cancelled or unsupported shape

Prevention

When it happens

Trigger: save() resolves to a truthy value that is neither a string nor an object with a `.path` string — e.g. a future/alternate dialog plugin returning { filePath: '...' } or a serialized File object. Practically rare with current @tauri-apps/plugin-dialog versions.

Common situations: Upgrading @tauri-apps/plugin-dialog across a major boundary that changes the return contract. A third-party dialog replacement. Misconfigured Tauri capabilities/permissions causing save() to resolve with an error object instead of throwing.

Related errors


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/da1a235d324c6d6c. Report an issue: GitHub.