remix-run/remix · error · DataTableValidationError

Invalid beforeDelete callback result for table "' + context.

Error message

Invalid beforeDelete callback result for table "' + context.tableName + '"

What it means

A beforeDelete hook may return nothing (undefined) to allow the delete, or { issues } to abort it with validation problems. Any other non-issue return value is rejected with a DataTableValidationError, because there is no meaningful 'value' transformation before a delete. The throw happens after the issues check, i.e. when the result is an unrecognized shape.

Source

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

): void {
  let callback = getTableBeforeDelete(table)

  if (!callback) {
    return
  }

  let callbackResult = callback(context)
  assertSynchronousCallbackResult(context.tableName, 'delete', 'beforeDelete', callbackResult)

  if (callbackResult === undefined) {
    return
  }

  if (hasIssues(callbackResult)) {
    throwValidationIssues(context.tableName, callbackResult.issues, 'delete', 'beforeDelete')
  }

  throw new DataTableValidationError(
    'Invalid beforeDelete callback result for table "' + context.tableName + '"',
    [{ message: 'Expected beforeDelete to return nothing or { issues }' }],
    {
      metadata: {
        table: context.tableName,
        operation: 'delete',
        source: 'beforeDelete',
      },
    },
  )
}

export function runAfterWriteHook<table extends AnyTable>(
  table: table,
  context: TableAfterWriteContext<TableRow<table>>,
): void {
  let callback = getTableAfterWrite(table)

View on GitHub (pinned to 9696913134)

Solutions

  1. Return nothing (undefined) from beforeDelete when the delete should proceed
  2. Return { issues: [{ message: 'reason' }] } to block the delete
  3. Audit beforeDelete callbacks for stray return statements

Example fix

// before
beforeDelete: (row) => {
  return row.deletable // boolean: invalid
}

// after
beforeDelete: (row) => {
  if (!row.deletable) {
    return { issues: [{ message: 'Row cannot be deleted' }] }
  }
  // return nothing to allow the delete
}
Defensive patterns

Strategy: type-guard

Type guard

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

Try / catch

try {
  await table.delete({ where })
} catch (error) {
  if (error instanceof DataTableValidationError && /beforeDelete callback result/.test(error.message)) {
    // hook bug: audit beforeDelete return values
  } else throw error
}

Prevention

When it happens

Trigger: A beforeDelete callback that returns true, a string, a promise-like value that is not an issue result, or any object without issues.

Common situations: Writing beforeDelete as if it could veto with a boolean (return true to proceed); porting middleware from another framework with different conventions; returning a value out of habit from beforeWrite-style hooks.

Related errors


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