0
# Exception Handling
1
2
Exception classes for managing state transition errors including general transition failures, concurrent modification conflicts, and invalid result states.
3
4
## Capabilities
5
6
### TransitionNotAllowed
7
8
Raised when a state transition is not allowed due to invalid source state, unmet conditions, or other transition constraints.
9
10
```python { .api }
11
class TransitionNotAllowed(Exception):
12
def __init__(self, *args, object=None, method=None, **kwargs):
13
"""
14
Exception raised when transition cannot be executed.
15
16
Parameters:
17
- *args: Standard exception arguments
18
- object: Model instance that failed transition (optional)
19
- method: Transition method that was called (optional)
20
- **kwargs: Additional exception arguments
21
22
Attributes:
23
- object: Model instance that failed transition
24
- method: Method that was attempted
25
"""
26
```
27
28
This exception is raised in several scenarios:
29
30
**Invalid Source State:**
31
```python
32
from django_fsm import TransitionNotAllowed
33
34
class Order(models.Model):
35
state = FSMField(default='pending')
36
37
@transition(field=state, source='confirmed', target='shipped')
38
def ship(self):
39
pass
40
41
# This will raise TransitionNotAllowed
42
order = Order.objects.create() # state is 'pending'
43
try:
44
order.ship() # Can only ship from 'confirmed' state
45
except TransitionNotAllowed as e:
46
print(f"Cannot transition: {e}")
47
print(f"Object: {e.object}")
48
print(f"Method: {e.method}")
49
```
50
51
**Unmet Condition:**
52
```python
53
def can_ship(instance):
54
return instance.payment_confirmed
55
56
class Order(models.Model):
57
state = FSMField(default='confirmed')
58
payment_confirmed = models.BooleanField(default=False)
59
60
@transition(field=state, source='confirmed', target='shipped', conditions=[can_ship])
61
def ship(self):
62
pass
63
64
# This will raise TransitionNotAllowed if payment not confirmed
65
order = Order.objects.create(state='confirmed', payment_confirmed=False)
66
try:
67
order.ship()
68
except TransitionNotAllowed as e:
69
print("Transition conditions not met")
70
```
71
72
### ConcurrentTransition
73
74
Raised when a transition cannot be executed because the object has become stale (state has been changed since it was fetched from the database).
75
76
```python { .api }
77
class ConcurrentTransition(Exception):
78
"""
79
Raised when transition cannot execute due to concurrent state modifications.
80
81
This exception indicates that the object's state was changed by another
82
process between when it was loaded and when the transition was attempted.
83
"""
84
```
85
86
Usage with ConcurrentTransitionMixin:
87
88
```python
89
from django_fsm import ConcurrentTransition, ConcurrentTransitionMixin
90
from django.db import transaction
91
92
class BankAccount(ConcurrentTransitionMixin, models.Model):
93
state = FSMField(default='active')
94
balance = models.DecimalField(max_digits=10, decimal_places=2)
95
96
@transition(field=state, source='active', target='frozen')
97
def freeze(self):
98
pass
99
100
def freeze_account(account_id):
101
try:
102
with transaction.atomic():
103
account = BankAccount.objects.get(pk=account_id)
104
account.freeze()
105
account.save() # May raise ConcurrentTransition
106
except ConcurrentTransition:
107
# Another process modified the account
108
print("Account was modified by another process")
109
# Implement retry logic or error handling
110
```
111
112
### InvalidResultState
113
114
Raised when a dynamic state resolution returns an invalid state value that is not in the allowed states list.
115
116
```python { .api }
117
class InvalidResultState(Exception):
118
"""
119
Raised when dynamic state resolution produces invalid result.
120
121
This occurs when using RETURN_VALUE or GET_STATE with restricted
122
allowed states, and the resolved state is not in the allowed list.
123
"""
124
```
125
126
Usage with dynamic states:
127
128
```python
129
from django_fsm import RETURN_VALUE, InvalidResultState
130
131
def calculate_priority_state():
132
# This might return an invalid state
133
return 'invalid_priority'
134
135
class Task(models.Model):
136
state = FSMField(default='new')
137
138
@transition(
139
field=state,
140
source='new',
141
target=RETURN_VALUE('low', 'medium', 'high')
142
)
143
def set_priority(self):
144
return calculate_priority_state()
145
146
try:
147
task = Task.objects.create()
148
task.set_priority() # May raise InvalidResultState
149
except InvalidResultState as e:
150
print(f"Invalid state returned: {e}")
151
```
152
153
## Exception Handling Patterns
154
155
### Basic Exception Handling
156
157
Handle different exception types appropriately:
158
159
```python
160
from django_fsm import TransitionNotAllowed, ConcurrentTransition
161
162
def safe_transition(instance, transition_method):
163
try:
164
transition_method()
165
instance.save()
166
return True, "Success"
167
168
except TransitionNotAllowed as e:
169
return False, f"Transition not allowed: {e}"
170
171
except ConcurrentTransition:
172
return False, "Object was modified by another process"
173
174
except Exception as e:
175
return False, f"Unexpected error: {e}"
176
177
# Usage
178
success, message = safe_transition(order, order.ship)
179
if success:
180
print("Order shipped successfully")
181
else:
182
print(f"Failed to ship order: {message}")
183
```
184
185
### Retry Logic for Concurrent Transitions
186
187
Implement retry logic for handling concurrent modifications:
188
189
```python
190
import time
191
import random
192
from django_fsm import ConcurrentTransition
193
194
def transition_with_retry(instance, transition_method, max_retries=3):
195
"""
196
Execute transition with exponential backoff retry for concurrent conflicts.
197
"""
198
for attempt in range(max_retries):
199
try:
200
with transaction.atomic():
201
# Refresh object to get latest state
202
instance.refresh_from_db()
203
204
# Attempt transition
205
transition_method()
206
instance.save()
207
return True
208
209
except ConcurrentTransition:
210
if attempt == max_retries - 1:
211
# Final attempt failed
212
raise
213
214
# Wait before retry with jitter
215
delay = 0.1 * (2 ** attempt) + random.uniform(0, 0.1)
216
time.sleep(delay)
217
218
return False
219
```
220
221
### Custom Exception Context
222
223
Add additional context to exceptions:
224
225
```python
226
class OrderTransitionError(TransitionNotAllowed):
227
"""Custom exception with additional order context."""
228
229
def __init__(self, message, order, transition_name, *args, **kwargs):
230
super().__init__(message, *args, **kwargs)
231
self.order = order
232
self.transition_name = transition_name
233
234
class Order(models.Model):
235
state = FSMField(default='pending')
236
237
@transition(field=state, source='confirmed', target='shipped')
238
def ship(self):
239
if not self.can_ship():
240
raise OrderTransitionError(
241
f"Order {self.id} cannot be shipped",
242
order=self,
243
transition_name='ship'
244
)
245
246
def handle_order_shipping(order):
247
try:
248
order.ship()
249
order.save()
250
except OrderTransitionError as e:
251
logger.error(
252
f"Shipping failed for order {e.order.id}: {e}",
253
extra={'order_id': e.order.id, 'transition': e.transition_name}
254
)
255
```
256
257
### Exception Logging and Monitoring
258
259
Implement comprehensive logging for state machine exceptions:
260
261
```python
262
import logging
263
from django_fsm import TransitionNotAllowed, ConcurrentTransition
264
265
logger = logging.getLogger('fsm_transitions')
266
267
def log_transition_error(instance, method_name, exception):
268
"""Log detailed information about transition failures."""
269
logger.error(
270
f"FSM transition failed: {exception}",
271
extra={
272
'model': instance.__class__.__name__,
273
'instance_id': instance.pk,
274
'current_state': getattr(instance, 'state', None),
275
'method': method_name,
276
'exception_type': exception.__class__.__name__
277
}
278
)
279
280
def monitored_transition(instance, transition_method, method_name):
281
"""Execute transition with comprehensive error logging."""
282
try:
283
transition_method()
284
instance.save()
285
286
logger.info(
287
f"FSM transition successful: {method_name}",
288
extra={
289
'model': instance.__class__.__name__,
290
'instance_id': instance.pk,
291
'new_state': getattr(instance, 'state', None),
292
'method': method_name
293
}
294
)
295
return True
296
297
except (TransitionNotAllowed, ConcurrentTransition) as e:
298
log_transition_error(instance, method_name, e)
299
return False
300
```
301
302
### Validation and Error Prevention
303
304
Prevent exceptions through validation:
305
306
```python
307
from django_fsm import can_proceed, has_transition_perm
308
309
def validate_and_execute_transition(instance, transition_method, user=None):
310
"""
311
Validate transition before execution to prevent exceptions.
312
"""
313
# Check if transition is possible
314
if not can_proceed(transition_method):
315
return False, "Transition not possible from current state"
316
317
# Check user permissions if provided
318
if user and not has_transition_perm(transition_method, user):
319
return False, "User does not have permission for this transition"
320
321
# Execute transition
322
try:
323
transition_method()
324
instance.save()
325
return True, "Transition successful"
326
327
except Exception as e:
328
return False, f"Transition failed: {e}"
329
330
# Usage in views
331
def order_action_view(request, order_id, action):
332
order = Order.objects.get(pk=order_id)
333
334
action_map = {
335
'ship': order.ship,
336
'cancel': order.cancel,
337
'refund': order.refund
338
}
339
340
method = action_map.get(action)
341
if not method:
342
return HttpResponseBadRequest("Invalid action")
343
344
success, message = validate_and_execute_transition(
345
order, method, request.user
346
)
347
348
if success:
349
messages.success(request, message)
350
else:
351
messages.error(request, message)
352
353
return redirect('order_detail', order_id=order.id)
354
```
355
356
### Testing Exception Scenarios
357
358
Test exception handling in your state machines:
359
360
```python
361
from django.test import TestCase
362
from django_fsm import TransitionNotAllowed, ConcurrentTransition
363
364
class OrderStateTests(TestCase):
365
def test_invalid_transition_raises_exception(self):
366
order = Order.objects.create(state='pending')
367
368
with self.assertRaises(TransitionNotAllowed) as cm:
369
order.ship() # Can't ship from pending
370
371
self.assertEqual(cm.exception.object, order)
372
self.assertEqual(cm.exception.method.__name__, 'ship')
373
374
def test_concurrent_modification_protection(self):
375
order = ConcurrentOrder.objects.create(state='pending')
376
377
# Simulate concurrent modification
378
ConcurrentOrder.objects.filter(pk=order.pk).update(state='confirmed')
379
380
with self.assertRaises(ConcurrentTransition):
381
order.ship()
382
order.save()
383
```