diff --git a/surround/assembler.py b/surround/assembler.py
index c028198..b3bb053 100644
--- a/surround/assembler.py
+++ b/surround/assembler.py
@@ -105,6 +105,8 @@ class Assembler(ABC):
 
         :param batch_mode: Whether batch mode should be used
         :type batch_mode: bool
+        :returns: whether the initialisation was successful
+        :rtype: bool
         """
 
         self.batch_mode = batch_mode
@@ -123,6 +125,9 @@ class Assembler(ABC):
                 self.finaliser.initialise(self.config)
         except Exception:
             LOGGER.exception("Failed initiating Assembler")
+            return False
+
+        return True
 
     def run(self, state=None, is_training=False):
         """
@@ -152,7 +157,12 @@ class Assembler(ABC):
         self.state = state
         try:
             self.validator.validate(self.state, self.config)
-            self.__run_pipeline(is_training)
+
+            if self.state.errors:
+                LOGGER.error("Error while validating")
+                LOGGER.error(self.state.errors)
+            else:
+                self.__run_pipeline(is_training)
         except Exception:
             LOGGER.exception("Failed running Assembler")
         finally:
@@ -169,15 +179,24 @@ class Assembler(ABC):
         """
 
         if self.pre_filters:
-            self.__execute_filters(self.pre_filters, self.state)
+            if not self.__execute_filters(self.pre_filters, self.state):
+                LOGGER.error("Failed running Assembler")
+                return
 
         if is_training:
             self.__execute_fit(self.state)
         else:
             self.__execute_main(self.state)
 
+        if self.state.errors:
+            LOGGER.error("Errors during executing the estimator")
+            LOGGER.error(self.state.errors)
+            return
+
         if self.post_filters:
-            self.__execute_filters(self.post_filters, self.state)
+            if not self.__execute_filters(self.post_filters, self.state):
+                LOGGER.error("Failed running Assembler")
+                return
 
         if (is_training or self.batch_mode) and self.visualiser:
             self.visualiser.visualise(self.state, self.config)
@@ -191,6 +210,8 @@ class Assembler(ABC):
         :type filters: list of :class:`surround.stage.Filter`
         :param state: the data being filtered
         :type state: :class:`surround.State`
+        :returns: whether execution succeeded
+        :rtype: bool
         """
 
         state.freeze()
@@ -202,14 +223,17 @@ class Assembler(ABC):
                 if state.errors:
                     LOGGER.error("Error during processing")
                     LOGGER.error(state.errors)
-                    break
+                    return False
+
             execution_time = datetime.now() - start_time
             state.execution_time = str(execution_time)
             LOGGER.info("Filter took %s secs", execution_time)
         except Exception:
             LOGGER.exception("Failed processing Surround Filters")
+            return False
 
         state.thaw()
+        return True
 
     def __execute_filter(self, stage, stage_data):
         """
diff --git a/templates/new/batch_runner.py.txt b/templates/new/batch_runner.py.txt
index c5a38ca..31b1a31 100644
--- a/templates/new/batch_runner.py.txt
+++ b/templates/new/batch_runner.py.txt
@@ -6,16 +6,18 @@ logging.basicConfig(level=logging.INFO)
 
 class BatchRunner(Runner):
     def run(self, is_training=False):
-        self.assembler.init_assembler(True)
-        data = AssemblerState()
+        if self.assembler.init_assembler(True):
+            data = AssemblerState()
 
-        # Load data to be processed
-        raw_data = "TODO: Load raw data here"
+            # Load data to be processed
+            raw_data = "TODO: Load raw data here"
 
-        # Setup input data
-        data.input_data = raw_data
+            # Setup input data
+            data.input_data = raw_data
 
-        # Run assembler
-        self.assembler.run(data, is_training)
+            # Run assembler
+            self.assembler.run(data, is_training)
 
-        logging.info("Batch Runner: %s", data.output_data)
+            logging.info("Batch Runner: %s", data.output_data)
+        else:
+            logging.exception("Failed to run the Assembler")

Throwing an exception doesn't stop the pipeline
diff --git a/surround/tests/surround_test.py b/surround/tests/surround_test.py
index 1924842..026b34f 100644
--- a/surround/tests/surround_test.py
+++ b/surround/tests/surround_test.py
@@ -8,6 +8,11 @@ test_text = "hello"
 
 class HelloStage(Estimator):
     def estimate(self, state, config):
+        if state.estimator_add_error:
+            state.errors.append("Error!!")
+        elif state.estimator_throw:
+            raise Exception("Error!!")
+
         state.text = test_text
         if "helloStage" in config:
             state.config_value = config["helloStage"]["suffix"]
@@ -16,13 +21,20 @@ class HelloStage(Estimator):
         print("No training implemented")
 
 
+# pylint: disable=too-many-instance-attributes
 class AssemblerState(State):
-    text = None
-    config_value = None
-    stage1 = None
-    stage2 = None
-    final_ran = False
-
+    def __init__(self):
+        self.errors = []
+        self.text = None
+        self.config_value = None
+        self.stage1 = None
+        self.final_ran = False
+        self.use_errors_instead = False
+        self.post_filter_ran = False
+        self.post_filter_throw = False
+        self.validator_add_error = False
+        self.estimator_throw = False
+        self.estimator_add_error = False
 
 class InputValidator(Validator):
     def validate(self, state, config):
@@ -35,9 +47,22 @@ class InputValidator(Validator):
         if state.stage1:
             raise ValueError("'stage1' is not None")
 
-        if state.stage2:
-            raise ValueError("'stage2' is not None")
+        if state.validator_add_error:
+            state.errors.append("Error!!")
+
+class BadFilter(Filter):
+    def initialise(self, config):
+        raise Exception("This will fail always")
 
+    def operate(self, state, config):
+        if state.use_errors_instead:
+            state.errors.append("This will fail always")
+        else:
+            raise Exception("This will fail always")
+
+class PostFilter(Filter):
+    def operate(self, state, config):
+        state.post_filter_ran = True
 
 class TestFinalStage(Filter):
     def operate(self, state, config):
@@ -94,3 +119,88 @@ class TestSurround(unittest.TestCase):
 
         # Finalizer should still be executed
         self.assertTrue(data.final_ran)
+
+    def test_assembler_init_pass(self):
+        assembler = Assembler("Pass test", InputValidator(), HelloStage())
+        self.assertTrue(assembler.init_assembler())
+
+    def test_assembler_init_fail(self):
+        assembler = Assembler("Fail test", InputValidator())
+        assembler.set_estimator(HelloStage(), [BadFilter()])
+        self.assertFalse(assembler.init_assembler())
+
+    def test_pipeline_stop_on_exception_estimator(self):
+        data = AssemblerState()
+        data.estimator_throw = True
+
+        assembler = Assembler("Fail test", InputValidator())
+        assembler.set_estimator(HelloStage(), [], [PostFilter()])
+
+        # This should fail to execute PostFilter
+        assembler.run(data)
+
+        self.assertFalse(data.post_filter_ran)
+
+    def test_pipeline_stop_on_error_estimator(self):
+        data = AssemblerState()
+        data.estimator_add_error = True
+
+        assembler = Assembler("Fail test", InputValidator())
+        assembler.set_estimator(HelloStage(), [], [PostFilter()])
+
+        # This should fail to execute PostFilter
+        assembler.run(data)
+
+        self.assertFalse(data.post_filter_ran)
+
+    def test_pipeline_stop_on_exception_filter(self):
+        data = AssemblerState()
+
+        assembler = Assembler("Fail test", InputValidator())
+        assembler.set_estimator(HelloStage(), [BadFilter()], [PostFilter()])
+
+        # This should fail to execute HelloStage
+        assembler.run(data)
+
+        self.assertIsNone(data.text)
+        self.assertFalse(data.post_filter_ran)
+
+    def test_pipeline_stop_on_error_filter(self):
+        # Use the errors list in state instead
+        data = AssemblerState()
+        data.use_errors_instead = True
+
+        assembler = Assembler("Fail test", InputValidator())
+        assembler.set_estimator(HelloStage(), [BadFilter()], [PostFilter()])
+
+        # This should fail to execute HelloStage & PostFilter
+        assembler.run(data)
+
+        self.assertIsNone(data.text)
+        self.assertFalse(data.post_filter_ran)
+
+    def test_pipeline_stop_on_exception_validator(self):
+        data = AssemblerState()
+        data.stage1 = "Now it will fail in the validator"
+
+        assembler = Assembler("Fail test", InputValidator())
+        assembler.set_estimator(HelloStage(), [], [PostFilter()])
+
+        # This should fail to execute HelloStage & PostFilter
+        assembler.run(data)
+
+        self.assertIsNone(data.text)
+        self.assertFalse(data.post_filter_ran)
+
+    def test_pipeline_stop_on_error_validator(self):
+        data = AssemblerState()
+        data.validator_add_error = True
+
+        assembler = Assembler("Fail test", InputValidator())
+        assembler.set_estimator(HelloStage(), [], [PostFilter()])
+
+        # This should fail to execute HelloStage & PostFilter
+        assembler.run(data)
+
+        self.assertIsNone(data.text)
+        self.assertFalse(data.post_filter_ran)

pip install -e .
parse_log_pytest
requirements.txt
pytest
apt-get update apt-get install -y gcc
3.6.5
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
certifi==2021.5.30
cloudpickle==2.2.1
doit==0.31.1
importlib-metadata==4.8.3
iniconfig==1.1.1
joblib==1.1.1
numpy==1.16.4
packaging==21.3
pandas==0.24.2
pluggy==1.0.0
py==1.11.0
pyaml==17.12.1
pyinotify==0.9.6
pyparsing==3.0.7
pytest==7.0.1
python-dateutil==2.9.0.post0
pytz==2025.2
PyYAML==6.0.1
scikit-learn==0.21.2
scipy==1.5.4
six==1.17.0
-e git+https://github.com/a2i2/surround.git@7cd0e7ec52bb2ae5dfb048a949fed137edcb2d31#egg=surround
tomli==1.2.3
typing_extensions==4.1.1
zipp==3.6.0

name: surround
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - libedit=3.1.20230828=h5eee18b_0
  - libffi=3.2.1=hf484d3e_1007
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.0.2u=h7b6447c_0
  - pip=21.2.2=py36h06a4308_0
  - python=3.6.5=hc3d631a_2
  - readline=7.0=h7b6447c_5
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.33.0=h62c20be_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - cloudpickle==2.2.1
      - doit==0.31.1
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - joblib==1.1.1
      - numpy==1.16.4
      - packaging==21.3
      - pandas==0.24.2
      - pluggy==1.0.0
      - py==1.11.0
      - pyaml==17.12.1
      - pyinotify==0.9.6
      - pyparsing==3.0.7
      - pytest==7.0.1
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - pyyaml==6.0.1
      - scikit-learn==0.21.2
      - scipy==1.5.4
      - six==1.17.0
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - zipp==3.6.0
prefix: /opt/conda/envs/surround

surround/tests/surround_test.py::TestSurround::test_assembler_init_pass surround/tests/surround_test.py::TestSurround::test_pipeline_stop_on_error_estimator surround/tests/surround_test.py::TestSurround::test_pipeline_stop_on_error_filter surround/tests/surround_test.py::TestSurround::test_pipeline_stop_on_error_validator surround/tests/surround_test.py::TestSurround::test_pipeline_stop_on_exception_filter
surround/tests/surround_test.py::TestSurround::test_assembler_init_fail surround/tests/surround_test.py::TestSurround::test_finaliser_fail_pipeline surround/tests/surround_test.py::TestSurround::test_finaliser_successful_pipeline surround/tests/surround_test.py::TestSurround::test_happy_path surround/tests/surround_test.py::TestSurround::test_pipeline_stop_on_exception_estimator surround/tests/surround_test.py::TestSurround::test_pipeline_stop_on_exception_validator surround/tests/surround_test.py::TestSurround::test_rejecting_attributes surround/tests/surround_test.py::TestSurround::test_surround_config
templates/new/batch_runner.py.txt surround/assembler.py
templates/new/batch_runner.py.txt surround/assembler.py