diff --git a/mobly/controller_manager.py b/mobly/controller_manager.py
index 75370dd..775ad3c 100644
--- a/mobly/controller_manager.py
+++ b/mobly/controller_manager.py
@@ -17,6 +17,7 @@ import copy
 import logging
 import yaml
 
+from mobly import expects
 from mobly import records
 from mobly import signals
 
@@ -152,10 +153,9 @@ class ControllerManager(object):
         # logging them.
         for name, module in self._controller_modules.items():
             logging.debug('Destroying %s.', name)
-            try:
+            with expects.expect_no_raises(
+                    'Exception occurred destroying %s.' % name):
                 module.destroy(self._controller_objects[name])
-            except:
-                logging.exception('Exception occurred destroying %s.', name)
         self._controller_objects = collections.OrderedDict()
         self._controller_modules = {}
 
@@ -204,8 +204,11 @@ class ControllerManager(object):
         """
         info_records = []
         for controller_module_name in self._controller_objects.keys():
-            record = self._create_controller_info_record(
-                controller_module_name)
-            if record:
-                info_records.append(record)
+            with expects.expect_no_raises(
+                    'Failed to collect controller info from %s' %
+                    controller_module_name):
+                record = self._create_controller_info_record(
+                    controller_module_name)
+                if record:
+                    info_records.append(record)
         return info_records

Errors in `teardown_class` are not properly recorded
If Mobly encounters an error in the cleanup stage of `teardown_class`, the error would not be recorded anywhere except the cli output, which makes debugging difficult.

This points to a bigger problem: the errors occurred in base test and runner themselves are not clearly handled.
diff --git a/mobly/base_test.py b/mobly/base_test.py
index 8ea90d3..2add2f2 100644
--- a/mobly/base_test.py
+++ b/mobly/base_test.py
@@ -41,6 +41,7 @@ STAGE_NAME_SETUP_CLASS = 'setup_class'
 STAGE_NAME_SETUP_TEST = 'setup_test'
 STAGE_NAME_TEARDOWN_TEST = 'teardown_test'
 STAGE_NAME_TEARDOWN_CLASS = 'teardown_class'
+STAGE_NAME_CLEAN_UP = 'clean_up'
 
 
 class Error(Exception):
@@ -374,9 +375,7 @@ class BaseTestClass(object):
                 self.summary_writer.dump(record.to_dict(),
                                          records.TestSummaryEntryType.RECORD)
         finally:
-            # Write controller info and summary to summary file.
-            self._record_controller_info()
-            self._controller_manager.unregister_controllers()
+            self._clean_up()
 
     def teardown_class(self):
         """Teardown function that will be called after all the selected tests in
@@ -846,10 +845,36 @@ class BaseTestClass(object):
             logging.info('Summary for test class %s: %s', self.TAG,
                          self.results.summary_str())
 
+    def _clean_up(self):
+        """The final stage of a test class execution."""
+        stage_name = STAGE_NAME_CLEAN_UP
+        record = records.TestResultRecord(stage_name, self.TAG)
+        record.test_begin()
+        self.current_test_info = runtime_test_info.RuntimeTestInfo(
+            stage_name, self.log_path, record)
+        expects.recorder.reset_internal_states(record)
+        with self._log_test_stage(stage_name):
+            # Write controller info and summary to summary file.
+            self._record_controller_info()
+            self._controller_manager.unregister_controllers()
+            if expects.recorder.has_error:
+                record.test_error()
+                record.update_record()
+                self.results.add_class_error(record)
+                self.summary_writer.dump(record.to_dict(),
+                                         records.TestSummaryEntryType.RECORD)
+
     def clean_up(self):
-        """A function that is executed upon completion of all tests selected in
+        """.. deprecated:: 1.8.1
+
+        Use `teardown_class` instead.
+
+        A function that is executed upon completion of all tests selected in
         the test class.
 
         This function should clean up objects initialized in the constructor by
         user.
+
+        Generally this should not be used as nothing should be instantiated
+        from the constructor of a test class.
         """
diff --git a/tests/mobly/base_test_test.py b/tests/mobly/base_test_test.py
index 1183133..8d77017 100755
--- a/tests/mobly/base_test_test.py
+++ b/tests/mobly/base_test_test.py
@@ -2001,6 +2001,53 @@ class BaseTestTest(unittest.TestCase):
             }
         }])
 
+    def test_record_controller_info_fail(self):
+        mock_test_config = self.mock_test_cls_configs.copy()
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        mock_ctrlr_2_config_name = mock_second_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        my_config = [{'serial': 'xxxx', 'magic': 'Magic'}]
+        mock_test_config.controller_configs[mock_ctrlr_config_name] = my_config
+        mock_test_config.controller_configs[
+            mock_ctrlr_2_config_name] = copy.copy(my_config)
+
+        class ControllerInfoTest(base_test.BaseTestClass):
+            """Registers two different controller types and modifies controller
+            info at runtime.
+            """
+
+            def setup_class(self):
+                device = self.register_controller(mock_controller)[0]
+                device.who_am_i = mock.MagicMock()
+                device.who_am_i.side_effect = Exception('Some failure')
+                second_controller = self.register_controller(
+                    mock_second_controller)[0]
+                # This should appear in recorded controller info.
+                second_controller.set_magic('haha')
+
+            def test_func(self):
+                pass
+
+        bt_cls = ControllerInfoTest(mock_test_config)
+        bt_cls.run()
+        info = bt_cls.results.controller_info[0]
+        self.assertEqual(len(bt_cls.results.controller_info), 1)
+        self.assertEqual(info.test_class, 'ControllerInfoTest')
+        self.assertEqual(info.controller_name, 'AnotherMagicDevice')
+        self.assertEqual(info.controller_info, [{
+            'MyOtherMagic': {
+                'magic': 'Magic',
+                'extra_magic': 'haha'
+            }
+        }])
+        record = bt_cls.results.error[0]
+        print(record.to_dict())
+        self.assertEqual(record.test_name, 'clean_up')
+        self.assertIsNotNone(record.begin_time)
+        self.assertIsNotNone(record.end_time)
+        expected_msg = ('Failed to collect controller info from '
+                        'mock_controller: Some failure')
+        self.assertEqual(record.details, expected_msg)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/tests/mobly/controller_manager_test.py b/tests/mobly/controller_manager_test.py
index 7c9f1b6..4bfc4ca 100755
--- a/tests/mobly/controller_manager_test.py
+++ b/tests/mobly/controller_manager_test.py
@@ -150,6 +150,16 @@ class ControllerManagerTest(unittest.TestCase):
         self.assertEqual(record.test_class, 'SomeClass')
         self.assertEqual(record.controller_name, 'MagicDevice')
 
+    @mock.patch('tests.lib.mock_controller.get_info')
+    def test_get_controller_info_records_error(self, mock_get_info_func):
+        mock_get_info_func.side_effect = Exception('Record info failed.')
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        c_manager.register_controller(mock_controller)
+        self.assertFalse(c_manager.get_controller_info_records())
+
     def test_get_controller_info_records(self):
         mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
         controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
@@ -189,6 +199,18 @@ class ControllerManagerTest(unittest.TestCase):
         self.assertFalse(c_manager._controller_objects)
         self.assertFalse(c_manager._controller_modules)
 
+    @mock.patch('tests.lib.mock_controller.destroy')
+    def test_unregister_controller_error(self, mock_destroy_func):
+        mock_ctrlr_config_name = mock_controller.MOBLY_CONTROLLER_CONFIG_NAME
+        controller_configs = {mock_ctrlr_config_name: ['magic1', 'magic2']}
+        c_manager = controller_manager.ControllerManager(
+            'SomeClass', controller_configs)
+        c_manager.register_controller(mock_controller)
+        mock_destroy_func.side_effect = Exception('Failed in destroy.')
+        c_manager.unregister_controllers()
+        self.assertFalse(c_manager._controller_objects)
+        self.assertFalse(c_manager._controller_modules)
+
     @mock.patch('tests.lib.mock_controller.destroy')
     def test_unregister_controller_without_registration(
             self, mock_destroy_func):

pip install -e .
parse_log_pytest
pytest
mock pytest pytz
apt-get update apt-get install -y adb
3.5
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
future==1.0.0
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
-e git+https://github.com/google/mobly.git@df643c7778fa7980fdb5f634dc4f2d6227fef7e3#egg=mobly
mock==5.2.0
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
portpicker==1.6.0
psutil==7.0.0
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pyserial==3.5
pytest==6.2.4
pytz==2025.2
PyYAML==6.0.1
timeout-decorator==0.5.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: mobly
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
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - future==1.0.0
      - mock==5.2.0
      - portpicker==1.6.0
      - psutil==7.0.0
      - pyserial==3.5
      - pytz==2025.2
      - pyyaml==6.0.1
      - timeout-decorator==0.5.0
prefix: /opt/conda/envs/mobly

tests/mobly/base_test_test.py::BaseTestTest::test_record_controller_info_fail tests/mobly/controller_manager_test.py::ControllerManagerTest::test_get_controller_info_records_error
tests/mobly/base_test_test.py::BaseTestTest::test_write_user_data
tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_on_fail tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_on_fail_from_setup_class tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_setup_class tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_setup_test tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_teardown_class tests/mobly/base_test_test.py::BaseTestTest::test_abort_all_in_test tests/mobly/base_test_test.py::BaseTestTest::test_abort_class_in_on_fail tests/mobly/base_test_test.py::BaseTestTest::test_abort_class_in_setup_test tests/mobly/base_test_test.py::BaseTestTest::test_abort_class_in_test tests/mobly/base_test_test.py::BaseTestTest::test_abort_class_setup_class tests/mobly/base_test_test.py::BaseTestTest::test_assert_equal_fail tests/mobly/base_test_test.py::BaseTestTest::test_assert_equal_fail_with_msg tests/mobly/base_test_test.py::BaseTestTest::test_assert_equal_pass tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_fail_with_noop tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_fail_with_wrong_error tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_fail_with_wrong_regex tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_pass tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_regex_fail_with_noop tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_regex_fail_with_wrong_error tests/mobly/base_test_test.py::BaseTestTest::test_assert_raises_regex_pass tests/mobly/base_test_test.py::BaseTestTest::test_assert_true tests/mobly/base_test_test.py::BaseTestTest::test_both_teardown_and_test_body_raise_exceptions tests/mobly/base_test_test.py::BaseTestTest::test_cli_test_selection_fail_by_convention tests/mobly/base_test_test.py::BaseTestTest::test_cli_test_selection_override_self_tests_list tests/mobly/base_test_test.py::BaseTestTest::test_current_test_info tests/mobly/base_test_test.py::BaseTestTest::test_current_test_info_in_setup_class tests/mobly/base_test_test.py::BaseTestTest::test_current_test_name tests/mobly/base_test_test.py::BaseTestTest::test_default_execution_of_all_tests tests/mobly/base_test_test.py::BaseTestTest::test_exception_objects_in_record tests/mobly/base_test_test.py::BaseTestTest::test_expect_equal tests/mobly/base_test_test.py::BaseTestTest::test_expect_false tests/mobly/base_test_test.py::BaseTestTest::test_expect_in_setup_class tests/mobly/base_test_test.py::BaseTestTest::test_expect_in_setup_class_and_on_fail tests/mobly/base_test_test.py::BaseTestTest::test_expect_in_setup_test tests/mobly/base_test_test.py::BaseTestTest::test_expect_in_teardown_class tests/mobly/base_test_test.py::BaseTestTest::test_expect_in_teardown_test tests/mobly/base_test_test.py::BaseTestTest::test_expect_multiple_fails tests/mobly/base_test_test.py::BaseTestTest::test_expect_no_op tests/mobly/base_test_test.py::BaseTestTest::test_expect_no_raises_custom_msg tests/mobly/base_test_test.py::BaseTestTest::test_expect_no_raises_default_msg tests/mobly/base_test_test.py::BaseTestTest::test_expect_true tests/mobly/base_test_test.py::BaseTestTest::test_expect_true_and_assert_true tests/mobly/base_test_test.py::BaseTestTest::test_expect_two_tests tests/mobly/base_test_test.py::BaseTestTest::test_explicit_pass tests/mobly/base_test_test.py::BaseTestTest::test_explicit_pass_but_teardown_test_raises_an_exception tests/mobly/base_test_test.py::BaseTestTest::test_fail tests/mobly/base_test_test.py::BaseTestTest::test_failure_in_procedure_functions_is_recorded tests/mobly/base_test_test.py::BaseTestTest::test_failure_to_call_procedure_function_is_recorded tests/mobly/base_test_test.py::BaseTestTest::test_generate_tests_call_outside_of_setup_generated_tests tests/mobly/base_test_test.py::BaseTestTest::test_generate_tests_dup_test_name tests/mobly/base_test_test.py::BaseTestTest::test_generate_tests_run tests/mobly/base_test_test.py::BaseTestTest::test_generate_tests_selected_run tests/mobly/base_test_test.py::BaseTestTest::test_implicit_pass tests/mobly/base_test_test.py::BaseTestTest::test_missing_requested_test_func tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_cannot_modify_original_record tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_executed_if_both_test_and_teardown_test_fails tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_executed_if_setup_class_fails_by_exception tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_executed_if_setup_test_fails_by_exception tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_executed_if_teardown_test_fails tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_executed_if_test_fails tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_raise_exception tests/mobly/base_test_test.py::BaseTestTest::test_on_fail_triggered_by_setup_class_failure_then_fail_too tests/mobly/base_test_test.py::BaseTestTest::test_on_pass_cannot_modify_original_record tests/mobly/base_test_test.py::BaseTestTest::test_on_pass_raise_exception tests/mobly/base_test_test.py::BaseTestTest::test_procedure_function_gets_correct_record tests/mobly/base_test_test.py::BaseTestTest::test_promote_extra_errors_to_termination_signal tests/mobly/base_test_test.py::BaseTestTest::test_record_controller_info tests/mobly/base_test_test.py::BaseTestTest::test_self_tests_list tests/mobly/base_test_test.py::BaseTestTest::test_self_tests_list_fail_by_convention tests/mobly/base_test_test.py::BaseTestTest::test_setup_and_teardown_execution_count tests/mobly/base_test_test.py::BaseTestTest::test_setup_class_fail_by_exception tests/mobly/base_test_test.py::BaseTestTest::test_setup_generated_tests_failure tests/mobly/base_test_test.py::BaseTestTest::test_setup_test_fail_by_exception tests/mobly/base_test_test.py::BaseTestTest::test_setup_test_fail_by_test_signal tests/mobly/base_test_test.py::BaseTestTest::test_skip tests/mobly/base_test_test.py::BaseTestTest::test_skip_if tests/mobly/base_test_test.py::BaseTestTest::test_skip_in_setup_test tests/mobly/base_test_test.py::BaseTestTest::test_teardown_class_fail_by_exception tests/mobly/base_test_test.py::BaseTestTest::test_teardown_class_raise_abort_all tests/mobly/base_test_test.py::BaseTestTest::test_teardown_test_assert_fail tests/mobly/base_test_test.py::BaseTestTest::test_teardown_test_executed_if_setup_test_fails tests/mobly/base_test_test.py::BaseTestTest::test_teardown_test_executed_if_test_fails tests/mobly/base_test_test.py::BaseTestTest::test_teardown_test_executed_if_test_pass tests/mobly/base_test_test.py::BaseTestTest::test_teardown_test_raise_exception tests/mobly/base_test_test.py::BaseTestTest::test_uncaught_exception tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_basic tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_default_None tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_default_overwrite tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_default_overwrite_by_optional_param_list tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_default_overwrite_by_required_param_list tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_optional tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_optional_missing tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_optional_with_default tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_required tests/mobly/base_test_test.py::BaseTestTest::test_unpack_userparams_required_missing tests/mobly/controller_manager_test.py::ControllerManagerTest::test_controller_record_exists_without_get_info tests/mobly/controller_manager_test.py::ControllerManagerTest::test_get_controller_info_record_not_serializable tests/mobly/controller_manager_test.py::ControllerManagerTest::test_get_controller_info_records tests/mobly/controller_manager_test.py::ControllerManagerTest::test_get_controller_info_records_empty tests/mobly/controller_manager_test.py::ControllerManagerTest::test_get_controller_info_without_registration tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_change_return_value tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_dup_register tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_less_than_min_number tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_no_config tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_no_config_for_not_required tests/mobly/controller_manager_test.py::ControllerManagerTest::test_register_controller_return_value tests/mobly/controller_manager_test.py::ControllerManagerTest::test_unregister_controller tests/mobly/controller_manager_test.py::ControllerManagerTest::test_unregister_controller_error tests/mobly/controller_manager_test.py::ControllerManagerTest::test_unregister_controller_without_registration tests/mobly/controller_manager_test.py::ControllerManagerTest::test_verify_controller_module tests/mobly/controller_manager_test.py::ControllerManagerTest::test_verify_controller_module_missing_attr tests/mobly/controller_manager_test.py::ControllerManagerTest::test_verify_controller_module_null_attr
mobly/controller_manager.py
mobly/controller_manager.py