Repository: PrefectHQ/prefect

Issue:
Discussion: order of State arguments
Eternal debate:

Should the signature of the `State` class be
```python
def __init__(self, result, message, ...):
```
or
```python
def __init__(self, message, result, ...):
```

I think it is more likely for states to have messages (nearly all) than results (usually just success, I believe). The current setup, with message as the second argument, therefore necessitates a lot of keyword arguments.


Discussion:
I don't think it's a bad idea to always use them as keyword arguments for the sake of clarity, but I agree that
```python
Success("Task run succeeded", result=5)
```
feels more natural than
```python
Success(5, message="Task run succeeded")
```
That's an excellent point -- I was more thinking in terms of States that are being used in purely informational ways, where it's natural to provide a single positional message:

```python
Failed("It didn't work!")
Pending("Waiting to start")
etc.
```

Resolving patch:
diff --git a/src/prefect/engine/runner.py b/src/prefect/engine/runner.py
--- a/src/prefect/engine/runner.py
+++ b/src/prefect/engine/runner.py
@@ -130,6 +130,6 @@ def handle_state_change(self, old_state: State, new_state: State) -> State:
             if raise_on_exception:
                 raise
             raise ENDRUN(
-                Failed("Exception raised while calling state handlers.", message=exc)
+                Failed("Exception raised while calling state handlers.", result=exc)
             )
         return new_state
diff --git a/src/prefect/engine/state.py b/src/prefect/engine/state.py
--- a/src/prefect/engine/state.py
+++ b/src/prefect/engine/state.py
@@ -24,8 +24,6 @@
 import prefect
 from prefect.utilities.json import Serializable
 
-MessageType = Union[str, Exception]
-
 
 class State(Serializable):
     """
@@ -42,14 +40,14 @@ class State(Serializable):
     ```
 
     Args:
-        - result (Any, optional): Defaults to `None`. A data payload for the state.
         - message (str or Exception, optional): Defaults to `None`. A message about the
             state, which could be an `Exception` (or [`Signal`](signals.html)) that caused it.
+        - result (Any, optional): Defaults to `None`. A data payload for the state.
     """
 
     color = "#000000"
 
-    def __init__(self, result: Any = None, message: MessageType = None) -> None:
+    def __init__(self, message: str = None, result: Any = None) -> None:
         self.result = result
         self.message = message
         self._timestamp = datetime.datetime.utcnow()
@@ -148,9 +146,9 @@ class Pending(State):
     Base Pending state; default state for new tasks.
 
     Args:
-        - result (Any, optional): Defaults to `None`. A data payload for the state.
         - message (str or Exception, optional): Defaults to `None`. A message about the
             state, which could be an `Exception` (or [`Signal`](signals.html)) that caused it.
+        - result (Any, optional): Defaults to `None`. A data payload for the state.
         - cached_inputs (dict): Defaults to `None`. A dictionary of input
         keys to values.  Used / set if the Task requires Retries.
     """
@@ -159,8 +157,8 @@ class Pending(State):
 
     def __init__(
         self,
+        message: str = None,
         result: Any = None,
-        message: MessageType = None,
         cached_inputs: Dict[str, Any] = None,
     ) -> None:
         super().__init__(result=result, message=message)
@@ -172,9 +170,9 @@ class CachedState(Pending):
     CachedState, which represents a Task whose outputs have been cached.
 
     Args:
-        - result (Any, optional): Defaults to `None`. A data payload for the state.
         - message (str or Exception, optional): Defaults to `None`. A message about the
             state, which could be an `Exception` (or [`Signal`](signals.html)) that caused it.
+        - result (Any, optional): Defaults to `None`. A data payload for the state.
         - cached_inputs (dict): Defaults to `None`. A dictionary of input
         keys to values.  Used / set if the Task requires Retries.
         - cached_result (Any): Defaults to `None`. Cached result from a
@@ -188,8 +186,8 @@ class CachedState(Pending):
 
     def __init__(
         self,
+        message: str = None,
         result: Any = None,
-        message: MessageType = None,
         cached_inputs: Dict[str, Any] = None,
         cached_result: Any = None,
         cached_parameters: Dict[str, Any] = None,
@@ -206,9 +204,9 @@ class Scheduled(Pending):
     Pending state indicating the object has been scheduled to run.
 
     Args:
-        - result (Any, optional): Defaults to `None`. A data payload for the state.
         - message (str or Exception, optional): Defaults to `None`. A message about the
             state, which could be an `Exception` (or [`Signal`](signals.html)) that caused it.
+        - result (Any, optional): Defaults to `None`. A data payload for the state.
         - start_time (datetime): time at which the task is scheduled to run
         - cached_inputs (dict): Defaults to `None`. A dictionary of input
             keys to values.  Used / set if the Task requires Retries.
@@ -218,8 +216,8 @@ class Scheduled(Pending):
 
     def __init__(
         self,
+        message: str = None,
         result: Any = None,
-        message: MessageType = None,
         start_time: datetime.datetime = None,
         cached_inputs: Dict[str, Any] = None,
     ) -> None:
@@ -232,9 +230,9 @@ class Retrying(Scheduled):
     Pending state indicating the object has been scheduled to be retried.
 
     Args:
-        - result (Any, optional): Defaults to `None`. A data payload for the state.
         - message (str or Exception, optional): Defaults to `None`. A message about the
             state, which could be an `Exception` (or [`Signal`](signals.html)) that caused it.
+        - result (Any, optional): Defaults to `None`. A data payload for the state.
         - start_time (datetime): time at which the task is scheduled to be retried
         - cached_inputs (dict): Defaults to `None`. A dictionary of input
             keys to values.  Used / set if the Task requires Retries.
@@ -247,8 +245,8 @@ class Retrying(Scheduled):
 
     def __init__(
         self,
+        message: str = None,
         result: Any = None,
-        message: MessageType = None,
         start_time: datetime.datetime = None,
         cached_inputs: Dict[str, Any] = None,
         run_count: int = None,
@@ -292,9 +290,9 @@ class Success(Finished):
     Finished state indicating success.
 
     Args:
-        - result (Any, optional): Defaults to `None`. A data payload for the state.
         - message (str or Exception, optional): Defaults to `None`. A message about the
             state, which could be an `Exception` (or [`Signal`](signals.html)) that caused it.
+        - result (Any, optional): Defaults to `None`. A data payload for the state.
         - cached (CachedState): a `CachedState` which can be used for future
             runs of this task (if the cache is still valid); this attribute should only be set
             by the task runner.
@@ -303,10 +301,7 @@ class Success(Finished):
     color = "#008000"
 
     def __init__(
-        self,
-        result: Any = None,
-        message: MessageType = None,
-        cached: CachedState = None,
+        self, message: str = None, result: Any = None, cached: CachedState = None
     ) -> None:
         super().__init__(result=result, message=message)
         self.cached = cached
@@ -339,5 +334,5 @@ class Skipped(Success):
 
     color = "#F0FFF0"
 
-    def __init__(self, result: Any = None, message: MessageType = None) -> None:
+    def __init__(self, message: str = None, result: Any = None) -> None:
         super().__init__(result=result, message=message)
diff --git a/src/prefect/engine/task_runner.py b/src/prefect/engine/task_runner.py
--- a/src/prefect/engine/task_runner.py
+++ b/src/prefect/engine/task_runner.py
@@ -347,7 +347,11 @@ def check_task_trigger(
             logging.debug("Unexpected error while running task.")
             if raise_on_exception:
                 raise exc
-            raise ENDRUN(TriggerFailed(message=exc))
+            raise ENDRUN(
+                TriggerFailed(
+                    "Unexpected error while checking task trigger.", result=exc
+                )
+            )
 
         return state
 
@@ -554,12 +558,18 @@ def get_task_run_state(
                 raise exc
             return exc.state
 
+        # inform user of timeout
+        except TimeoutError as exc:
+            if raise_on_exception:
+                raise exc
+            return Failed("Task timed out during execution.", result=exc)
+
         # Exceptions are trapped and turned into Failed states
         except Exception as exc:
             logging.debug("Unexpected error while running task.")
             if raise_on_exception:
                 raise exc
-            return Failed(message=exc)
+            return Failed("Unexpected error while running task.", result=exc)
 
         return Success(result=result, message="Task run succeeded.")
 


Test patch:
diff --git a/tests/core/test_task_map.py b/tests/core/test_task_map.py
--- a/tests/core/test_task_map.py
+++ b/tests/core/test_task_map.py
@@ -332,7 +332,8 @@ def test_map_allows_for_retries(executor):
     assert states.is_failed()  # division by zero
 
     old = states.result[divved]
-    assert [s.result for s in old] == [None, 1.0, 0.5]
+    assert [s.result for s in old][1:] == [1.0, 0.5]
+    assert isinstance(old[0].result, ZeroDivisionError)
 
     old[0] = prefect.engine.state.Success(result=100)
     states = f.run(
diff --git a/tests/engine/test_flow_runner.py b/tests/engine/test_flow_runner.py
--- a/tests/engine/test_flow_runner.py
+++ b/tests/engine/test_flow_runner.py
@@ -794,7 +794,8 @@ def test_flow_runner_handles_timeouts(executor):
 
     state = FlowRunner(flow=flow).run(return_tasks=[res], executor=executor)
     assert state.is_failed()
-    assert isinstance(state.result[res].message, TimeoutError)
+    assert "timed out" in state.result[res].message
+    assert isinstance(state.result[res].result, TimeoutError)
 
 
 @pytest.mark.skipif(
@@ -808,7 +809,7 @@ def test_flow_runner_handles_timeout_error_with_mproc(mproc):
 
     state = FlowRunner(flow=flow).run(return_tasks=[res], executor=mproc)
     assert state.is_failed()
-    assert isinstance(state.result[res].message, AssertionError)
+    assert isinstance(state.result[res].result, AssertionError)
 
 
 @pytest.mark.parametrize("executor", ["local", "mthread", "sync"], indirect=True)
@@ -825,7 +826,7 @@ def test_flow_runner_handles_mapped_timeouts(executor):
     assert mapped_states[0].is_successful()
     for fstate in mapped_states[1:]:
         assert fstate.is_failed()
-        assert isinstance(fstate.message, TimeoutError)
+        assert isinstance(fstate.result, TimeoutError)
 
 
 handler_results = collections.defaultdict(lambda: 0)
diff --git a/tests/engine/test_state.py b/tests/engine/test_state.py
--- a/tests/engine/test_state.py
+++ b/tests/engine/test_state.py
@@ -42,12 +42,19 @@ def test_create_state_with_no_args(cls):
 
 
 @pytest.mark.parametrize("cls", all_states)
-def test_create_state_with_positional_data_arg(cls):
-    state = cls(1)
+def test_create_state_with_kwarg_data_arg(cls):
+    state = cls(result=1)
     assert state.result == 1
     assert state.message is None
 
 
+@pytest.mark.parametrize("cls", all_states)
+def test_create_state_with_positional_message_arg(cls):
+    state = cls("i am a string")
+    assert state.message == "i am a string"
+    assert state.result is None
+
+
 @pytest.mark.parametrize("cls", all_states)
 def test_create_state_with_data_and_message(cls):
     state = cls(message="x", result="y")
@@ -156,9 +163,9 @@ def test_state_equality_ignores_message():
 
 
 def test_state_equality_with_nested_states():
-    s1 = State(result=Success(1))
-    s2 = State(result=Success(2))
-    s3 = State(result=Success(1))
+    s1 = State(result=Success(result=1))
+    s2 = State(result=Success(result=2))
+    s3 = State(result=Success(result=1))
     assert s1 != s2
     assert s1 == s3
 
diff --git a/tests/engine/test_task_runner.py b/tests/engine/test_task_runner.py
--- a/tests/engine/test_task_runner.py
+++ b/tests/engine/test_task_runner.py
@@ -219,11 +219,11 @@ def test_running_task_that_already_has_finished_state_doesnt_run():
 def test_task_runner_preserves_error_type():
     task_runner = TaskRunner(ErrorTask())
     state = task_runner.run()
-    msg = state.message
-    if isinstance(msg, Exception):
-        assert type(msg).__name__ == "ValueError"
+    exc = state.result
+    if isinstance(exc, Exception):
+        assert type(exc).__name__ == "ValueError"
     else:
-        assert "ValueError" in msg
+        assert "ValueError" in exc
 
 
 def test_task_runner_raise_on_exception_when_task_errors():
@@ -306,7 +306,8 @@ def test_task_runner_can_handle_timeouts_by_default():
     sleeper = SlowTask(timeout=datetime.timedelta(seconds=1))
     state = TaskRunner(sleeper).run(inputs=dict(secs=2))
     assert state.is_failed()
-    assert isinstance(state.message, TimeoutError)
+    assert "timed out" in state.message
+    assert isinstance(state.result, TimeoutError)
 
 
 def test_task_runner_handles_secrets():
@@ -565,7 +566,7 @@ def trigger(states):
                 state=state, upstream_states_set={Success()}
             )
         assert isinstance(exc.value.state, TriggerFailed)
-        assert isinstance(exc.value.state.message, ZeroDivisionError)
+        assert isinstance(exc.value.state.result, ZeroDivisionError)
 
 
 class TestCheckTaskPending:
@@ -711,7 +712,7 @@ def fn():
             state=state, inputs={}, timeout_handler=None
         )
         assert new_state.is_failed()
-        assert isinstance(new_state.message, ZeroDivisionError)
+        assert isinstance(new_state.result, ZeroDivisionError)
 
     def test_inputs(self):
         @prefect.task
diff --git a/tests/tasks/test_strings.py b/tests/tasks/test_strings.py
--- a/tests/tasks/test_strings.py
+++ b/tests/tasks/test_strings.py
@@ -42,8 +42,8 @@ def test_string_formatter_fails_in_expected_ways():
     res = f.run(return_tasks=[t1, t2])
 
     assert res.is_failed()
-    assert isinstance(res.result[t1].message, KeyError)
-    assert isinstance(res.result[t2].message, IndexError)
+    assert isinstance(res.result[t1].result, KeyError)
+    assert isinstance(res.result[t2].result, IndexError)
 
 
 def test_jinja_template_simply_formats():