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

  1. Return { value: row } from afterRead on success
  2. Return { issues: [{ message, path }] } to surface read-time validation failures
  3. 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

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


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