Is raising exceptions safe within 'before' callback of a transitions.Machine model?
<p>I am using the <a href="https://github.com/tyarkoni/transitions" rel="nofollow">transitions</a> FSM library. Imagine having an application FSM using the following code:</p>

<pre><code>from transitions import Machine

import os

class Application(object):

  states = ["idle", "data_loaded"]

  def __init__(self):
    self.data = None
    machine = Machine(model=self, states=Application.states, initial="idle")
    machine.add_transition("filename_dropped",
                           source="idle",
                           dest="data_loaded",
                           before="load_data",
                           conditions="is_valid_filename")
    self.machine = machine

  def drop_filename(self, filename):
    try:
      self.filename_dropped(filename)
    except IOError as exc:
      print "Oops: %s" % str(exc)

  def load_data(self, filename):
    with open(filename) as file:
      self.data = file.read()

  def is_valid_filename(self, filename):
    return os.path.isfile(filename)
</code></pre>

<p>It can throw an <code>IOError</code> within load_data. My question is whether it is safe to raise exceptions (either implicitly like in this example or explicitly) from within <code>before</code> callbacks? In case of an <code>IOError</code> the transition is not taking place, the state of this example remains <code>idle</code> and any <code>after</code> callbacks are not being invoked. However, I wonder whether the internal state of the machine instance might get corrupted.</p>

<p>Additional question: Are there any better ways to signal errors with concrete information to the application? In this very example I could use the condition to load the file, but this seems ugly and I would need some additional attribute to keep track of the error etc.</p>

<p>Thanks for any help or advice.</p>

<blockquote>
  <p>However, I wonder whether the internal state of the machine instance might get corrupted.</p>
</blockquote>

<p>It is fine to work with raising Exceptions in callback functions unless you plan to use the <code>queued</code> feature of transitions.</p>

<p>Transitions are executed in the following order:</p>

<p><code>prepare -&gt; conditions -&gt; before -&gt; on_exit -&gt; set_state -&gt; on_enter -&gt; after</code></p>

<p>If anything before <code>set_state</code> raises an Exception or a function in <code>conditions</code> does not return <code>True</code>, the transition is halted.</p>

<p>Your model might be in an undefined state though. If you rely on some 'clean up' or 'tear down' in <code>State.on_enter</code> or <code>after</code>:</p>

<pre><code>from transitions import Machine
class Model:
    def __init__(self):
        self.busy = False
    def before(self):
        self.busy = True
        raise Exception('oops')
    def after(self):
        # if state transition is done, reset busy
        self.busy = False

model = Model()
m = Machine(model, states=['A','B'], initial='A',
            transitions=[{'trigger':'go', 'source':'A', 'dest':'B',
                          'before':'before', 'after':'after'}])
try:
  model.go()
except Exception as e:
  print "Exception: %s" % e # Exception: oops
print "State: %s" % model.state # State: A
print "Model busy: %r" % model.busy # Model busy: True
</code></pre>

<blockquote>
  <p>Are there any better ways to signal errors with concrete information to the application?</p>
</blockquote>

<p>It depends on what you want to achieve. Raising Errors/Exceptions usually halts the execution of a current task. In my oppinion this is pretty much THE way to propagate issues. If you want to handle the error and represent the error as a state, I would not consider using <code>conditions</code> ugly. Valid transitions with the same <code>trigger</code> are evaluated in the order they were added. With this in mind and the use of <code>unless</code> which is negated notation for <code>conditions</code>, your code could look like this: </p>

<pre><code>from transitions import Machine
import os


class Application(object):
  states = ["idle", "data_loaded", "filename_invalid", "data_invalid"]

  transitions = [
    {'trigger': 'filename_dropped', 'source': 'idle', 
     'dest': 'filename_invalid', 'unless': 'is_valid_filename'},
    {'trigger':'filename_dropped', 'source': 'idle',
     'dest':'data_invalid', 'unless': 'is_valid_data'},
    {'trigger':'filename_dropped', 'source': 'idle',
     'dest':'data_loaded'}
  ]

  def __init__(self):
    self.data = None
    machine = Machine(model=self, states=Application.states,
                      transitions=Application.transitions, initial="idle")
    self.machine = machine

  def drop_filename(self, filename):
      self.filename_dropped(filename)
      if self.is_data_loaded():
          print "Data loaded"

  # renamed load_data
  def is_valid_data(self, filename):
    try:
      with open(filename) as file:
        self.data = file.read()
    except IOError as exc:
      print "File loading error: %s" % str(exc)
      return False
    return True

  def is_valid_filename(self, filename):
    return os.path.isfile(filename)

app = Application()
app.drop_filename('test.txt')
# &gt;&gt;&gt; Data loaded
print app.state
# &gt;&gt;&gt; data_loaded
app.to_idle()
app.drop_filename('missing.txt')
print app.state
# &gt;&gt;&gt; filename_invalid
app.to_idle()
app.drop_filename('secret.txt')
# &gt;&gt;&gt; File loading error: [Errno 13] Permission denied: 'secret.txt'
print app.state
# &gt;&gt;&gt; data_invalid
</code></pre>

<p>This will create this state machine:</p>

<p><a href="http://i.stack.imgur.com/IKfQL.png" rel="nofollow"><img src="http://i.stack.imgur.com/IKfQL.png" alt="enter image description here"></a></p>

python transitions fsm