{"record":{"id":"83c6f52dc9518bb7","repo":"remix-run/remix","slug":"invalid-callbackname-callback-result-for-t","errorCode":null,"errorMessage":"Invalid ' + callbackName + ' callback result for table \"' + tableName + '\"","messagePattern":"Invalid ' \\+ callbackName \\+ ' callback result for table \"' \\+ tableName \\+ '\"","errorType":"validation","errorClass":"DataTableValidationError","httpStatus":null,"severity":"error","filePath":"packages/data-table/src/lib/database/write-lifecycle.ts","lineNumber":447,"sourceCode":"    metadata: {\n      table: tableName,\n      operation,\n      ...(source ? { source } : {}),\n    },\n  })\n}\n\nfunction assertSynchronousCallbackResult(\n  tableName: string,\n  operation: TableLifecycleOperation,\n  callbackName: LifecycleCallbackSource,\n  value: unknown,\n): void {\n  if (!isPromiseLike(value)) {\n    return\n  }\n\n  throw new DataTableValidationError(\n    'Invalid ' + callbackName + ' callback result for table \"' + tableName + '\"',\n    [{ message: callbackName + ' callbacks must be synchronous and cannot return a Promise' }],\n    {\n      metadata: {\n        table: tableName,\n        operation,\n        source: callbackName,\n      },\n    },\n  )\n}\n\nfunction isPromiseLike(value: unknown): value is PromiseLike<unknown> {\n  return (\n    (typeof value === 'object' || typeof value === 'function') &&\n    value !== null &&\n    'then' in value &&\n    typeof (value as { then?: unknown }).then === 'function'","sourceCodeStart":429,"sourceCodeEnd":465,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table/src/lib/database/write-lifecycle.ts#L429-L465","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nbeforeWrite: async (values) => {\n  return { value: { ...values, password: await hash(values.password) } }\n}\n\n// after\nbeforeWrite: (values) => {\n  return { value: { ...values, password: precomputedHash } }\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"function isPromiseLike(value: unknown): value is Promise<unknown> {\n  return typeof value === 'object' && value !== null && typeof (value as { then?: unknown }).then === 'function'\n}","tryCatchPattern":"try {\n  await table.create({ values })\n} catch (error) {\n  if (error instanceof DataTableValidationError && /must be synchronous/.test(error.message)) {\n    // make the hook sync; move async work outside\n  } else throw error\n}","preventionTips":["Never mark lifecycle callbacks as async","Precompute async values (hashes, lookups) before invoking the query","Return plain values only: { value }, { issues }, or undefined"],"tags":["data-table","lifecycle-hooks","async","synchronous-contract"],"backgroundTag":"async-callback-in-sync-hook","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}