or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

dynamic-states.mdexceptions.mdfield-types.mdindex.mdmodel-mixins.mdsignals.mdtransitions.mdvisualization.md

exceptions.mddocs/

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

```