RollbackOutsideTransaction
RollbackOutsideTransaction
Severity: error | Type: LiquidHtml
{% rollback %} only has meaning inside {% transaction %}. Reached anywhere else, the platform raises rollback performed outside of transaction — so this is a guaranteed runtime error, not a style preference.
Why the offense lands on the caller
A partial cannot know whether it is running inside a transaction: the same file is correct under one call site and broken under another.
{% comment %}fine — the caller opens the transaction{% endcomment %}
{% transaction %}{% function _ = 'commands/order/place' %}{% endtransaction %}
{% comment %}raises — the same partial, no transaction{% endcomment %}
{% function _ = 'commands/order/place' %}
The transaction state is only known at the root of a render tree, so that is where this check reports. A rollback written in a partial is never reported where it is written. Instead the check follows {% render %}, {% include %}, {% theme_render_rc %}, {% function %} and {% background %} calls from the files whose state it does know, and reports on the call site, naming the chain it followed:
Rendering 'order/wrapper' reaches a {% rollback %} that is not inside a {% transaction %} block
(order/wrapper → order/place). At runtime the platform raises "rollback performed outside of transaction".
Which files have a known state
| File type | State at the top of the file | Why |
|---|---|---|
| Page, Layout, Email, ApiCall, Sms | no transaction | Nothing wraps the render — a bare rollback here always raises |
| Migration | inside a transaction | Migrations run inside one already, so a bare {% rollback %} in a migration is valid and is not reported |
Partial (views/partials/, lib/) |
unknown | The caller decides — this is why the check descends render trees |
| FormConfiguration, Authorization | unknown | A form can be submitted programmatically from a GraphQL mutation, so its callbacks and policies may run inside the caller's transaction |
Two tags interrupt the transaction
{% background %} never inherits a transaction. A background job is only queued after the transaction that scheduled it commits, so it always runs outside one. A rollback under a {% background %} block, or in a partial a {% background %} schedules, is reported from any file — including a partial and including code already wrapped in {% transaction %}.
{% content_for %} bodies are never reported. The body runs where the matching {% yield %} is, which may be in another file, so where it is written proves nothing about its transaction state.
Examples
✗ Incorrect Code Example (Avoid using this):
A rollback with no transaction around it:
{% comment %}app/views/pages/orders/create.liquid{% endcomment %}
{% liquid
function order = 'commands/order/create', total: 100
unless order.valid
rollback
endunless
%}
A partial that rolls back, called without a transaction:
{% comment %}app/views/pages/orders/create.liquid{% endcomment %}
{% function order = 'commands/order/place', total: 100 %}
{% comment %}app/lib/commands/order/place.liquid{% endcomment %}
{% liquid
function order = 'modules/core/commands/execute', mutation_name: 'order/create', object: object
unless order.valid
rollback
endunless
%}
A rollback inside a background job:
{% transaction %}
{% background %}
{% comment %}the job runs after this transaction commits, so it is never inside one{% endcomment %}
{% rollback %}
{% endbackground %}
{% endtransaction %}
✓ Correct Code Example (Use this instead):
Wrap the work that must roll back:
{% liquid
transaction
function order = 'commands/order/create', total: 100
unless order.valid
rollback
endunless
endtransaction
%}
Or open the transaction at the call site, so the partial can stay reusable:
{% comment %}app/views/pages/orders/create.liquid{% endcomment %}
{% transaction %}
{% function order = 'commands/order/place', total: 100 %}
{% endtransaction %}
Give a background job its own transaction:
{% background %}
{% transaction %}
{% function order = 'commands/order/place', total: 100 %}
{% endtransaction %}
{% endbackground %}
Configuration
The default configuration for this check:
RollbackOutsideTransaction:
enabled: true
severity: error
Disabling This Check
There should be no reason to disable this check. Every offense it reports is code that raises when it runs. It is deliberately conservative: a partial's rollback is only reported once a caller proves it runs outside a transaction, a partial named by a variable is never followed, and file types whose transaction state cannot be determined are left alone — so a silence is not a guarantee that a rollback is correctly placed.