{"record":{"id":"c278184c5a11bbed","repo":"apify/crawlee","slug":"operation-cannot-be-used-inside-a-storage-trans-c27818","errorCode":null,"errorMessage":"${operation} cannot be used inside a storage transaction: ${reason} If you really need it, wrap the call in withDirectStorageAccess(() => ...) - operations performed there are applied immediately and are not rolled back.","messagePattern":"(.+?) cannot be used inside a storage transaction: (.+?) If you really need it, wrap the call in withDirectStorageAccess\\(\\(\\) => \\.\\.\\.\\) - operations performed there are applied immediately and are not rolled back\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/core/src/storages/transaction.ts","lineNumber":429,"sourceCode":"export function snapshotValue<T>(value: T): T {\n    try {\n        return structuredClone(value);\n    } catch {\n        return JSON.parse(JSON.stringify(value));\n    }\n}\n\n/**\n * The guard for operations that cannot be performed inside a storage transaction: throws when one is\n * active, and performs the per-operation cancellation check either way.\n * @internal\n */\nexport function rejectOperationInTransaction(operation: string, reason = 'it cannot be rolled back.'): void {\n    if (activeStorageTransaction() === undefined) {\n        return;\n    }\n\n    throw operationRejectedInTransaction(operation, reason);\n}\n\n/**\n * Builds the \"operation not allowed in a transaction\" error, for a call site that has already\n * established a transaction is active and so wants to `throw` unconditionally.\n * @internal\n */\nexport function operationRejectedInTransaction(operation: string, reason = 'it cannot be rolled back.'): Error {\n    return new Error(\n        `${operation} cannot be used inside a storage transaction: ${reason} ` +\n            'If you really need it, wrap the call in withDirectStorageAccess(() => ...) - operations ' +\n            'performed there are applied immediately and are not rolled back.',\n    );\n}\n","sourceCodeStart":411,"sourceCodeEnd":444,"githubUrl":"https://github.com/apify/crawlee/blob/dbe57fb09ca607ad59dcf998f3925ef9ac3bb26c/packages/core/src/storages/transaction.ts#L411-L444","documentation":"`rejectOperationInTransaction(operation, reason)` is a shared guard that checks whether a storage transaction is currently active (via `activeStorageTransaction()`); if one is, it throws the standard 'cannot be used inside a storage transaction' error built by `operationRejectedInTransaction`, otherwise it returns silently. It protects operations that cannot be rolled back — RequestQueue operations like `drop`, `clearCache`, `fetchNextRequest`, `markRequestAsHandled`, `reclaimRequest`, and `purge` — because mutating them inside a transaction would survive a rollback or break the commit replay.","triggerScenarios":"Calling any guarded RequestQueue method (`drop()`, `purge()`, `reclaimRequest()`, `fetchNextRequest()`, `markRequestAsHandled()`, `clearCache()`) from within an active storage transaction opened with the transactions API; the guard detects `activeStorageTransaction() !== undefined` and throws.","commonSituations":"Crawler/queue maintenance code (purging a queue before a run, dropping a finished queue) accidentally executed inside a transactional callback that also persists state; custom request-handling logic calling `markRequestAsHandled` or `reclaimRequest` inside `useStorageTransaction`; migrating older code to the transaction-aware persistence layer.","solutions":["Wrap the guarded call in `withDirectStorageAccess(() => ...)` if you explicitly want it applied immediately and never rolled back.","Restructure the code so queue mutations happen outside the transaction — e.g. drop/purge the queue before opening the transaction or after it commits.","If the mutation should be transactional, replace it with an equivalent operation that the transaction API supports (e.g. `addRequest` instead of `drop`/`purge` side effects)."],"exampleFix":"// before\nawait useStorageTransaction(async () => {\n    await requestQueue.drop(); // throws\n});\n\n// after\nawait withDirectStorageAccess(() => requestQueue.drop());","handlingStrategy":"validation","validationCode":"import { activeStorageTransaction } from '@crawlee/core/storages/transaction';\n// Guard before mutating the request queue\nif (activeStorageTransaction() !== undefined) {\n  await withDirectStorageAccess(() => requestQueue.purge());\n} else {\n  await requestQueue.purge();\n}","typeGuard":"function isInsideStorageTransaction(): boolean {\n  return activeStorageTransaction() !== undefined;\n}","tryCatchPattern":"try {\n  await requestQueue.drop();\n} catch (err) {\n  if (String(err.message).includes('cannot be used inside a storage transaction')) {\n    await withDirectStorageAccess(() => requestQueue.drop());\n  } else {\n    throw err;\n  }\n}","preventionTips":["Treat RequestQueue mutations (drop, purge, reclaimRequest, markRequestAsHandled, fetchNextRequest, clearCache) as non-transactional: always call them outside transaction blocks.","Perform queue lifecycle operations (drop/purge) before opening or after committing a transaction.","Use `withDirectStorageAccess` deliberately when you truly need immediate, non-rollbackable queue mutations.","Add a lint/test check that no guarded queue methods are called inside `useStorageTransaction` callbacks."],"tags":["storage","transaction","request-queue","rollback"],"backgroundTag":"operation-not-allowed-in-storage-transaction","analyzedSha":"dbe57fb09ca607ad59dcf998f3925ef9ac3bb26c","analyzedAt":"2026-08-30T22:22:28.328Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}