{"record":{"id":"f6aa7fa3b7a3417e","repo":"remix-run/remix","slug":"invalid-afterread-callback-result-for-table-t","errorCode":null,"errorMessage":"Invalid afterRead callback result for table \"' + tableName + '\"","messagePattern":"Invalid afterRead callback result for table \"' \\+ tableName \\+ '\"","errorType":"validation","errorClass":"DataTableValidationError","httpStatus":null,"severity":"error","filePath":"packages/data-table/src/lib/database/write-lifecycle.ts","lineNumber":116,"sourceCode":"  if (!callback || rows.length === 0) {\n    return rows\n  }\n\n  let tableName = getTableName(table)\n\n  return rows.map((row) => {\n    let callbackResult = callback({\n      tableName,\n      value: row as Partial<TableRow<table>>,\n    })\n    assertSynchronousCallbackResult(tableName, 'read', 'afterRead', callbackResult)\n\n    if (hasIssues(callbackResult)) {\n      throwValidationIssues(tableName, callbackResult.issues, 'read', 'afterRead')\n    }\n\n    if (!hasValue(callbackResult)) {\n      throw new DataTableValidationError(\n        'Invalid afterRead callback result for table \"' + tableName + '\"',\n        [{ message: 'Expected afterRead to return { value } or { issues }' }],\n        {\n          metadata: {\n            table: tableName,\n            operation: 'read',\n            source: 'afterRead',\n          },\n        },\n      )\n    }\n\n    return normalizeReadObject(tableName, callbackResult.value)\n  })\n}\n\nexport function applyAfterReadHooksToLoadedRows(\n  table: AnyTable,","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table/src/lib/database/write-lifecycle.ts#L98-L134","documentation":"After an afterRead hook runs for each row, its return value must be either a result object containing { value } or one containing { issues }. If the hook returned something with neither shape (undefined, a bare row, a boolean), a DataTableValidationError is thrown telling you what afterRead must return. This protects the read pipeline from silently dropping or corrupting rows.","triggerScenarios":"An afterRead callback that returns nothing, returns the row directly (not wrapped in { value }), or returns an arbitrary value like true/null.","commonSituations":"Writing afterRead hooks that mutate in place and forget to return { value: row }; porting hooks from another convention where returning the row directly is valid; early returns inside the callback that fall through to undefined.","solutions":["Return { value: row } from afterRead on success","Return { issues: [{ message, path }] } to surface read-time validation failures","Ensure every code path in the callback, including early returns, returns one of the two shapes"],"exampleFix":"// before\nafterRead: (row) => {\n  row.fullName = row.first + ' ' + row.last\n}\n\n// after\nafterRead: (row) => {\n  return { value: { ...row, fullName: row.first + ' ' + row.last } }\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isValidAfterReadResult(result: unknown): boolean {\n  return (\n    result == null ||\n    (typeof result === 'object' && ('value' in (result as object) || 'issues' in (result as object)))\n  )\n}","tryCatchPattern":"try {\n  rows = await query()\n} catch (error) {\n  if (error instanceof DataTableValidationError && /afterRead callback result/.test(error.message)) {\n    // fix the hook; not a data problem\n  } else throw error\n}","preventionTips":["Always return { value: row } or { issues } from afterRead","Type hook results explicitly: (row) => { value: Row } | { issues: Issue[] }","Add a lint/unit test that exercises every hook's return paths"],"tags":["data-table","after-read","lifecycle-hooks","validation"],"backgroundTag":"invalid-hook-return-shape","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}