actualbudget/actual · error · GenericFileError

Could not update File

Error message

Could not update File

What it means

GenericFileError thrown by FilesService.update() when the SQL UPDATE statement affects zero rows (res.changes !== 1). Since the caller already fetched the raw file before building the update, zero changes means the row vanished or the WHERE clause matched nothing.

Source

Thrown at packages/sync-server/src/app-sync/services/files-service.ts:256

      params.push(fileUpdate.encryptMeta);
    }
    if (fileUpdate.syncVersion !== undefined) {
      updates.push('sync_version = ?');
      params.push(fileUpdate.syncVersion);
    }
    if (fileUpdate.deleted !== undefined) {
      updates.push('deleted = ?');
      params.push(boolToInt(fileUpdate.deleted));
    }

    if (updates.length > 0) {
      query += ' ' + updates.join(', ') + ' WHERE id = ?';
      params.push(id);

      const res = this.accountDb.mutate(query, params);

      if (res.changes !== 1) {
        throw new GenericFileError('Could not update File', { id });
      }
    }

    // Return the modified object
    const rawFile = this.getRaw(id);
    if (!rawFile) {
      throw new GenericFileError('File not found', { id });
    }
    return this.validate(rawFile);
  }

  getRaw(fileId: FileId): RawFile | null {
    return this.accountDb.first(`SELECT * FROM files WHERE id = ?`, [fileId]);
  }

  validate(rawFile: RawFile) {
    const fileId = rawFile.id;
    if (!isValidFileId(fileId)) {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Check that the file ID still exists before updating (SELECT via getRaw or get).
  2. Retry the update after re-fetching the file if a concurrent delete removed it.
  3. Ensure only one sync-server instance writes to the account SQLite DB.
  4. Return 404-style handling in the route so clients can re-create or stop tracking the file.

Example fix

// before
await filesService.update(fileId, { name });   // may throw 'Could not update File'
// after
if (filesService.getRaw(fileId)) {
  await filesService.update(fileId, { name });
}
Defensive patterns

Strategy: retry

Validate before calling

// verify the row exists and retry-worthy before updating
const exists = filesService.getRaw(id);
if (!exists) throw new Error(`Cannot update file ${id}: no longer exists`);

Type guard

function isUpdateFailure(err: unknown): err is Error {
  return err instanceof Error && err.message === 'Could not update File';
}

Try / catch

for (let attempt = 0; attempt < 3; attempt++) {
  try {
    return filesService.update(id, patch);
  } catch (err) {
    if (isUpdateFailure(err) && filesService.getRaw(id)) continue; // transient race
    throw err;
  }
}
throw new Error(`Failed to update file ${id} after retries`);

Prevention

When it happens

Trigger: update(id, ...) called concurrently with a delete of the same file (row removed between read and write); passing an id whose row no longer exists; a race between two server instances writing the same file row.

Common situations: Two devices deleting/updating the same budget file simultaneously; admin purging users/files while a client sync is in flight; SQLite locking or restart wiping in-flight state in ephemeral deployments.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/69486a5cbc54f0ef. Report an issue: GitHub.