remix-run/remix · error · DataTableValidationError

Invalid beforeWrite callback result for table "' + tableName

Error message

Invalid beforeWrite callback result for table "' + tableName + '"

What it means

A beforeWrite hook must return either a result with { value } (the possibly-transformed row) or one with { issues }. If it returns a value with neither field, a DataTableValidationError is thrown during validateWriteValues, because the library cannot tell whether the write should proceed or fail.

Source

Thrown at packages/data-table/src/lib/database/write-lifecycle.ts:293

): Record<string, unknown> {
  let tableName = getTableName(table)
  let normalizedInput = normalizeWriteObject(table, values, operation)
  let beforeWrite = getTableBeforeWrite(table)

  if (beforeWrite) {
    let beforeWriteResult = beforeWrite({
      operation,
      tableName,
      value: normalizedInput as Partial<TableRow<table>>,
    })
    assertSynchronousCallbackResult(tableName, operation, 'beforeWrite', beforeWriteResult)

    if (hasIssues(beforeWriteResult)) {
      throwValidationIssues(tableName, beforeWriteResult.issues, operation, 'beforeWrite')
    }

    if (!hasValue(beforeWriteResult)) {
      throw new DataTableValidationError(
        'Invalid beforeWrite callback result for table "' + tableName + '"',
        [{ message: 'Expected beforeWrite to return { value } or { issues }' }],
        {
          metadata: {
            table: tableName,
            operation,
            source: 'beforeWrite',
          },
        },
      )
    }

    normalizedInput = normalizeWriteObject(table, beforeWriteResult.value, operation, 'beforeWrite')
  }

  let validator = getTableValidator(table)

  if (!validator) {

View on GitHub (pinned to 9696913134)

Solutions

  1. Return { value: newValues } from beforeWrite, including every path
  2. Return { issues: [...] } to reject the write with validation issues
  3. Double-check early returns and conditional branches inside the hook

Example fix

// before
beforeWrite: (values) => {
  values.email = values.email.toLowerCase()
}

// after
beforeWrite: (values) => {
  return { value: { ...values, email: values.email.toLowerCase() } }
}
Defensive patterns

Strategy: type-guard

Type guard

function isValidBeforeWriteResult(result: unknown): boolean {
  return (
    typeof result === 'object' &&
    result !== null &&
    ('value' in result || 'issues' in result)
  )
}

Try / catch

try {
  await table.create({ values })
} catch (error) {
  if (error instanceof DataTableValidationError && /beforeWrite callback result/.test(error.message)) {
    // hook bug: ensure every path returns { value } or { issues }
  } else throw error
}

Prevention

When it happens

Trigger: beforeWrite callbacks that mutate and return undefined, return the row unwrapped, or return an arbitrary truthy value.

Common situations: Transforming input in beforeWrite (hashing passwords, normalizing email) and forgetting to return the new object; copy-pasting afterRead-style hooks where in-place mutation was okay; early-return code paths.

Related errors


AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27). Data as JSON: /api/errors/02fdf9401d15b90b. Report an issue: GitHub.