remix-run/remix · error · DataTableValidationError
Invalid afterRead callback result for table "' + tableName +
Error message
Invalid afterRead callback result for table "' + tableName + '"
What it means
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.
Source
Thrown at packages/data-table/src/lib/database/write-lifecycle.ts:116
if (!callback || rows.length === 0) {
return rows
}
let tableName = getTableName(table)
return rows.map((row) => {
let callbackResult = callback({
tableName,
value: row as Partial<TableRow<table>>,
})
assertSynchronousCallbackResult(tableName, 'read', 'afterRead', callbackResult)
if (hasIssues(callbackResult)) {
throwValidationIssues(tableName, callbackResult.issues, 'read', 'afterRead')
}
if (!hasValue(callbackResult)) {
throw new DataTableValidationError(
'Invalid afterRead callback result for table "' + tableName + '"',
[{ message: 'Expected afterRead to return { value } or { issues }' }],
{
metadata: {
table: tableName,
operation: 'read',
source: 'afterRead',
},
},
)
}
return normalizeReadObject(tableName, callbackResult.value)
})
}
export function applyAfterReadHooksToLoadedRows(
table: AnyTable,View on GitHub (pinned to 9696913134)
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
Example fix
// before
afterRead: (row) => {
row.fullName = row.first + ' ' + row.last
}
// after
afterRead: (row) => {
return { value: { ...row, fullName: row.first + ' ' + row.last } }
} Defensive patterns
Strategy: type-guard
Type guard
function isValidAfterReadResult(result: unknown): boolean {
return (
result == null ||
(typeof result === 'object' && ('value' in (result as object) || 'issues' in (result as object)))
)
} Try / catch
try {
rows = await query()
} catch (error) {
if (error instanceof DataTableValidationError && /afterRead callback result/.test(error.message)) {
// fix the hook; not a data problem
} else throw error
} Prevention
- 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
When it happens
Trigger: An afterRead callback that returns nothing, returns the row directly (not wrapped in { value }), or returns an arbitrary value like true/null.
Common situations: 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.
Related errors
- Invalid beforeDelete callback result for table "' + context.
- Invalid beforeWrite callback result for table "' + tableName
- Invalid validator result for table "' + tableName + '"
- upsert requires at least one value
- insertMany() requires at least one explicit value across the
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/f6aa7fa3b7a3417e.
Report an issue: GitHub.