Lessons from the Django Atomic Block Exception Handling
Today we dive into this obscure exception: Unhandled exception: An error occurred in the current transaction. You can’t execute queries…

Today we dive into this obscure exception: Unhandled exception: An error occurred in the current transaction. You can’t execute queries until the end of the ‘atomic’ block.
What Happened
On a sunny Wednesday, we were excited to release a new version of our service to production, which has around 1k~5k active requests per second. As usual, we monitored for unexpected exceptions that happened, and this popped up a few times: Unhandled exception: An error occurred in the current transaction. You can’t execute queries until the end of the ‘atomic’ block.
The root cause was found quickly, a race condition existed in how one of the tables arehandled, but what troubled us was the strange message that was not helping at all!
Our Original Code
class A(Model):
id: int
class B(Model):
id: int
a: OneToOneField(A, on_delete=models.CASCADE)
def send_event(a):
print(f"{a.id} deleted")
def delete_a(a):
with @transaction.atomic():
try:
a.delete()
send_event(a)
except A.NotFound:
pass # it is ok
other_db_query()
Note a race condition could already happen, when 2 requests delete the same instance of A, it is likely that we would send the event twice.
A refactor that creates this obscure exception
We thought making send_event
into a signal could help make the main business more clear:
class A(Model):
id: int
class B(Model):
id: int
a: OneToOneField(A, on_delete=models.CASCADE)
@receiver(post_delete, sender=B)
def send_event(b):
a = b.a
print(f"{a.id} deleted")
def delete_a(a):
with @transaction.atomic():
try:
a.delete()
except A.NotFound:
pass # it is ok
other_db_query()

What we expected to happen upon a race condition should be
- Request 1 delete A instance, cascade delete B, triggers signal in
send_event
- Request 2 delete the same A instance, does the same thing, but finishes first.
- In request 1,
send_event
should raise A.NotFound in linea = b.a
- The exception should be handled as disregarded in
delete_a
try-catch block. - Other query should proceed as normal.
But in fact, we receive this Unhandled exception: An error occurred in the current transaction. You can’t execute queries until the end of the ‘atomic’ block. And the line of error happens in some other DB query
. Why?
Django’s Official Warning
In Django’s official document: https://docs.djangoproject.com/en/5.1/topics/db/transactions/#controlling-transactions-explicitly
It states that one should Avoid catching exceptions inside atomic
!
Why? Because inside an atomic session, when a DB query failed, it not only raises exception, but it mark the transaction state as being corrupted. This is so that during the __exit__
handling of a with transaction.atomic()
context, it can properly rollback the session.
In our case, the exception raised from a = b.a
is caught indeed! but the atomic session state has been marked as corrupted. So in some other DB query
, we have this obscure exception which is not helpful, and took us a long time to find the root cause.
What should be done in this situation?
Following Django’s document suggestion, if we are to handle an exception within a atomic session, it is best to create a nested session and try catch it. This update will fix the issue
@receiver(post_delete, sender=B)
def send_event(b):
try:
with transaction.atomic():
a = b.a
print(f"{a.id} deleted")
except A.NotFound:
pass
Since a NotFound exception is not something that actually breaks the database, we let the inner session handles the rollback (which does nothing), and set the session state back to normal, some that later DB query can be executed normally.
Wrapping It All Up: The Art of Taming Transactions
In high-concurrency systems, even caught exceptions can corrupt transactions. Django’s atomic blocks demand careful handling — use nested transactions to isolate risks and follow framework warnings. Resilient code isn’t just functional; it’s designed for real-world complexity. Remember, every obscure error is a chance to learn and improve.
“Unless the Lord builds the house, those who build it labor in vain.”
— Psalm 127:1