{"record":{"id":"f8b9518bad846aea","repo":"Budibase/budibase","slug":"unable-to-retrieve-row-row-id-after-saving","errorCode":null,"errorMessage":"Unable to retrieve row ${row._id} after saving.","messagePattern":"Unable to retrieve row (.+?) after saving\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/server/src/api/controllers/row/staticFormula.ts","lineNumber":168,"sourceCode":"  let enrichedRow = await outputProcessing(source, cloneDeep(row), {\n    squash: false,\n  })\n  // use enriched row to generate formulas for saving, specifically only use as context\n  row = await processFormulas(table, row, {\n    dynamic: false,\n    contextRows: [enrichedRow],\n  })\n\n  if (updateAIColumns) {\n    row = await processAIColumns(table, row, {\n      contextRows: [enrichedRow],\n    })\n  }\n\n  await db.put(row)\n  const retrieved = await db.tryGet<Row>(row._id)\n  if (!retrieved) {\n    throw new Error(`Unable to retrieve row ${row._id} after saving.`)\n  }\n\n  delete enrichedRow._rev\n  enrichedRow = mergeRows(retrieved, enrichedRow)\n  enrichedRow = await processFormulas(table, enrichedRow, {\n    dynamic: false,\n  })\n\n  // this updates the related formulas in other rows based on the relations to this row\n  if (updateFormula) {\n    await updateRelatedFormula(table, enrichedRow)\n  }\n  const squashed = await linkRows.squashLinks(source, enrichedRow)\n  return { row: enrichedRow, squashed, table }\n}\n","sourceCodeStart":150,"sourceCodeEnd":184,"githubUrl":"https://github.com/Budibase/budibase/blob/a81a902e9a8fe55b467d106765f6638f12e35c49/packages/server/src/api/controllers/row/staticFormula.ts#L150-L184","documentation":"After writing a row with db.put(), finaliseRow immediately re-reads the row with db.tryGet() so it can merge the stored document (with its new _rev) back into the enriched row for formula processing. If the read returns nothing the save pipeline cannot continue, so it throws. This normally indicates the row vanished between the put and the get, or the _id is inconsistent.","triggerScenarios":"A concurrent delete removed the row between db.put(row) and db.tryGet(row._id) in finaliseRow at packages/server/src/api/controllers/row/staticFormula.ts:168; a database replication/consistency lag where the freshly written doc is not yet readable; a corrupted or missing _id on the row document.","commonSituations":"Race conditions where two automations operate on the same row (one saving, one deleting); CouchDB sync issues in clustered deployments; save hooks (updateRelatedFormula) cascading onto rows that another process just removed.","solutions":["Retry the save — a transient read-after-write miss usually succeeds on a second attempt.","Check for concurrent deletes/automations racing on the same row and serialize them.","Verify the workspace DB is healthy (replication, CouchDB logs) if this happens repeatedly.","Inspect the row document's _id for corruption or undefined values before saving."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// before saving, ensure row has a valid _id\nif (!row._id) {\n  throw new Error(\"Row must have an _id before saving\")\n}","typeGuard":"function isStoredRow(row: Row | undefined): row is Row {\n  return !!row && typeof row._id === \"string\"\n}","tryCatchPattern":"try {\n  await saveRow(row)\n} catch (err) {\n  if (err.message.startsWith(\"Unable to retrieve row\")) {\n    // transient read-after-write miss — retry once with backoff\n    await new Promise(r => setTimeout(r, 100))\n    return saveRow(row)\n  }\n  throw err\n}","preventionTips":["Avoid concurrent automations writing and deleting the same row.","Monitor CouchDB health/replication lag.","Always persist rows with a defined _id and _rev."],"tags":["database","race-condition","persistence"],"backgroundTag":"read-after-write-missing","analyzedSha":"a81a902e9a8fe55b467d106765f6638f12e35c49","analyzedAt":"2026-08-29T01:03:10.972Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}