Repository: PrefectHQ/prefect

Issue:
Try/Catch accidentally suppresses errors in serializers
## Description
<!-- A clear description of the bug -->
https://github.com/PrefectHQ/prefect/blob/master/src/prefect/engine/task_runner.py#L892 
A Flow using the PandasSerializer and the S3Result failed to serialize a `datetime.timedelta` in a column. But, that returned an `ArrowNotImplementedError`. the serializer didn't catch it, the result didn't catch it, and it fell through to the line pointed at which allowed the Task to Succeed in a case where it should have failed.


## Expected Behavior
<!-- What did you expect to happen instead? -->
Task should have failed


## Reproduction
<!-- A minimal example that exhibits the behavior. -->




## Environment
<!-- Any additional information about your environment

Optionally run `prefect diagnostics` from the command line and paste the information here. -->


Comments:

Patch:
diff --git a/changes/pr3964.yaml b/changes/pr3964.yaml
new file mode 100644
index 0000000000..2c4023eb50
--- /dev/null
+++ b/changes/pr3964.yaml
@@ -0,0 +1,2 @@
+fix:
+  - "Properly handle `NotImplementedError` exceptions raised by a result's serializer - [#3964](https://github.com/PrefectHQ/prefect/pull/3964)"
diff --git a/src/prefect/engine/result/base.py b/src/prefect/engine/result/base.py
index 8e6139a37d..05a6888424 100644
--- a/src/prefect/engine/result/base.py
+++ b/src/prefect/engine/result/base.py
@@ -29,6 +29,12 @@ from prefect.engine.serializers import PickleSerializer, Serializer
 from prefect.utilities import logging
 
 
+# Subclass of `NotImplementedError` to make it easier to distinguish this error
+# in consuming code
+class ResultNotImplementedError(NotImplementedError):
+    """Indicates a Result feature isn't implemented"""
+
+
 class Result:
     """
     A representation of the result of a Prefect task; this class contains
@@ -182,7 +188,7 @@ class Result:
         Returns:
             - bool: whether or not the target result exists.
         """
-        raise NotImplementedError(
+        raise ResultNotImplementedError(
             "Not implemented on the base Result class - if you are seeing this error you "
             "might be trying to use features that require choosing a Result subclass; "
             "see https://docs.prefect.io/core/concepts/results.html"
@@ -198,7 +204,7 @@ class Result:
         Returns:
             - Any: The value saved to the result.
         """
-        raise NotImplementedError(
+        raise ResultNotImplementedError(
             "Not implemented on the base Result class - if you are seeing this error you "
             "might be trying to use features that require choosing a Result subclass; "
             "see https://docs.prefect.io/core/concepts/results.html"
@@ -217,7 +223,7 @@ class Result:
         Returns:
             - Result: a new result object with the appropriately formatted location destination
         """
-        raise NotImplementedError(
+        raise ResultNotImplementedError(
             "Not implemented on the base Result class - if you are seeing this error you "
             "might be trying to use features that require choosing a Result subclass; "
             "see https://docs.prefect.io/core/concepts/results.html"
diff --git a/src/prefect/engine/task_runner.py b/src/prefect/engine/task_runner.py
index 501a00cbec..c1642076d6 100644
--- a/src/prefect/engine/task_runner.py
+++ b/src/prefect/engine/task_runner.py
@@ -18,7 +18,7 @@ import prefect
 from prefect import config
 from prefect.core import Edge, Task
 from prefect.engine import signals
-from prefect.engine.result import Result
+from prefect.engine.result.base import Result, ResultNotImplementedError
 from prefect.engine.runner import ENDRUN, Runner, call_state_handlers
 from prefect.engine.state import (
     Cached,
@@ -889,7 +889,7 @@ class TaskRunner(Runner):
                     **raw_inputs,
                 }
                 result = self.result.write(value, **formatting_kwargs)
-            except NotImplementedError:
+            except ResultNotImplementedError:
                 result = self.result.from_value(value=value)
         else:
             result = self.result.from_value(value=value)
@@ -978,7 +978,7 @@ class TaskRunner(Runner):
                         loop_result = self.result.write(
                             loop_result.value, **formatting_kwargs
                         )
-                    except NotImplementedError:
+                    except ResultNotImplementedError:
                         pass
 
                 state_context = {"_loop_count": prefect.context["task_loop_count"]}


Test patch:
diff --git a/tests/engine/result/test_base.py b/tests/engine/result/test_base.py
index dce845dca3..aaa2ddff18 100644
--- a/tests/engine/result/test_base.py
+++ b/tests/engine/result/test_base.py
@@ -7,6 +7,7 @@ from unittest.mock import MagicMock
 
 import prefect
 from prefect.engine.result import Result, NoResultType
+from prefect.engine.result.base import ResultNotImplementedError
 
 
 class TestInitialization:
@@ -31,12 +32,12 @@ class TestInitialization:
 def test_has_abstract_interfaces(abstract_interface: str):
     """
     Tests to make sure that calling the abstract interfaces directly
-    on the base `Result` class results in `NotImplementedError`s.
+    on the base `Result` class results in `ResultNotImplementedError`s.
     """
     r = Result(value=3)
 
     func = getattr(r, abstract_interface)
-    with pytest.raises(NotImplementedError):
+    with pytest.raises(ResultNotImplementedError):
         func(None)