remix-run/remix · error · DataTableValidationError
Invalid ' + callbackName + ' callback result for table "' +
Error message
Invalid ' + callbackName + ' callback result for table "' + tableName + '"
What it means
All synchronous lifecycle callbacks (beforeWrite, validate, afterRead, beforeDelete, afterWrite, afterDelete) must not return a Promise. If a Promise-like value comes back, a DataTableValidationError is thrown naming the callback, because the library executes these hooks synchronously and cannot await them. Marking the callback async or returning an await expression triggers this immediately.
Source
Thrown at packages/data-table/src/lib/database/write-lifecycle.ts:447
metadata: {
table: tableName,
operation,
...(source ? { source } : {}),
},
})
}
function assertSynchronousCallbackResult(
tableName: string,
operation: TableLifecycleOperation,
callbackName: LifecycleCallbackSource,
value: unknown,
): void {
if (!isPromiseLike(value)) {
return
}
throw new DataTableValidationError(
'Invalid ' + callbackName + ' callback result for table "' + tableName + '"',
[{ message: callbackName + ' callbacks must be synchronous and cannot return a Promise' }],
{
metadata: {
table: tableName,
operation,
source: callbackName,
},
},
)
}
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
return (
(typeof value === 'object' || typeof value === 'function') &&
value !== null &&
'then' in value &&
typeof (value as { then?: unknown }).then === 'function'View on GitHub (pinned to 9696913134)
Solutions
- Make the callback synchronous: remove async and precompute async work outside the hook
- Perform async pre/post processing in your own code before/after the query instead of inside the hook
- Return plain values ({ value } / { issues } / undefined), never Promises
Example fix
// before
beforeWrite: async (values) => {
return { value: { ...values, password: await hash(values.password) } }
}
// after
beforeWrite: (values) => {
return { value: { ...values, password: precomputedHash } }
} Defensive patterns
Strategy: type-guard
Type guard
function isPromiseLike(value: unknown): value is Promise<unknown> {
return typeof value === 'object' && value !== null && typeof (value as { then?: unknown }).then === 'function'
} Try / catch
try {
await table.create({ values })
} catch (error) {
if (error instanceof DataTableValidationError && /must be synchronous/.test(error.message)) {
// make the hook sync; move async work outside
} else throw error
} Prevention
- Never mark lifecycle callbacks as async
- Precompute async values (hashes, lookups) before invoking the query
- Return plain values only: { value }, { issues }, or undefined
When it happens
Trigger: Declaring a lifecycle hook as async (values) => {...}; returning a Promise from a helper inside the hook (e.g. returning bcrypt.hash(...) instead of awaiting it); returning this query inside afterRead.
Common situations: Adding await-based logic (hashing, lookups, fetch calls) to hooks that must stay synchronous; refactoring a hook to async during a feature addition; forgetting that afterRead runs per-row inside a sync loop.
Related errors
- Invalid afterRead callback result for table "' + tableName +
- Invalid beforeDelete callback result for table "' + context.
- Invalid beforeWrite callback result for table "' + tableName
- Invalid validator result for table "' + tableName + '"
- Unknown transaction token: + token.id
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/83c6f52dc9518bb7.
Report an issue: GitHub.