{"record":{"id":"da1a235d324c6d6c","repo":"jamiepine/voicebox","slug":"failed-to-resolve-save-path-from-dialog","errorCode":null,"errorMessage":"Failed to resolve save path from dialog","messagePattern":"Failed to resolve save path from dialog","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"tauri/src/platform/filesystem.ts","lineNumber":19,"sourceCode":"import type { FileFilter, PlatformFilesystem } from '@/platform/types';\n\nexport const tauriFilesystem: PlatformFilesystem = {\n  async saveFile(filename: string, blob: Blob, filters?: FileFilter[]) {\n    const { save } = await import('@tauri-apps/plugin-dialog');\n    const { writeFile } = await import('@tauri-apps/plugin-fs');\n\n    const filePath = await save({\n      defaultPath: filename,\n      filters: filters || [],\n    });\n\n    if (!filePath) return; // User cancelled the dialog\n\n    const resolvedPath =\n      typeof filePath === 'string' ? filePath : (filePath as { path: string }).path;\n\n    if (!resolvedPath) {\n      throw new Error('Failed to resolve save path from dialog');\n    }\n\n    const arrayBuffer = await blob.arrayBuffer();\n    await writeFile(resolvedPath, new Uint8Array(arrayBuffer));\n  },\n\n  async openPath(path: string) {\n    const { open } = await import('@tauri-apps/plugin-shell');\n    await open(path);\n  },\n\n  async pickDirectory(title: string) {\n    const { open } = await import('@tauri-apps/plugin-dialog');\n    const selected = await open({ directory: true, title });\n    if (!selected) return null;\n    const dir = typeof selected === 'string' ? selected : (selected as { path: string }).path;\n    return dir || null;\n  },","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/tauri/src/platform/filesystem.ts#L1-L37","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Confirm @tauri-apps/plugin-dialog version matches the documented return shape (string on Tauri v1, { path } on some v2 paths).","Log the raw resolved value when this throws to learn the actual shape: console.error('save returned', filePath).","Broaden the normalization to also read filePath / name / path fields before giving up.","Ensure the Tauri capability for dialog:allow-save is granted in capability files."],"exampleFix":"// before\nconst resolvedPath =\n  typeof filePath === 'string' ? filePath : (filePath as { path: string }).path;\nif (!resolvedPath) {\n  throw new Error('Failed to resolve save path from dialog');\n}\n// after\nconst resolvedPath =\n  typeof filePath === 'string'\n    ? filePath\n    : (filePath as { path?: string; filePath?: string }).path ??\n      (filePath as { filePath?: string }).filePath;\nif (!resolvedPath || typeof resolvedPath !== 'string') {\n  throw new Error(`Failed to resolve save path from dialog: ${JSON.stringify(filePath)}`);\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"type SaveDialogResult = string | { path: string } | { filePath: string } | null;\nfunction resolveSavePath(v: SaveDialogResult): string | null {\n  if (typeof v === 'string') return v;\n  if (v && typeof v === 'object') return v.path ?? v.filePath ?? null;\n  return null;\n}","tryCatchPattern":"let resolvedPath: string | null = null;\ntry {\n  const filePath = await save({ defaultPath: filename, filters: filters || [] });\n  resolvedPath = resolveSavePath(filePath as SaveDialogResult);\n} catch (e) {\n  throw new Error(`Save dialog failed: ${(e as Error).message}`);\n}\nif (!resolvedPath) return; // user cancelled or unsupported shape","preventionTips":["Pin @tauri-apps/plugin-dialog to a version whose return shape your normalize() expects.","Grant dialog:allow-save in the Tauri capability file.","Distinguish 'user cancelled' (null) from 'unexpected shape' in logging."],"tags":["tauri","desktop","filesystem","dialog","typescript","platform"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}