diff --git a/devlib/__init__.py b/devlib/__init__.py
index f4b3ac4..d290b1f 100644
--- a/devlib/__init__.py
+++ b/devlib/__init__.py
@@ -15,7 +15,7 @@
 
 from devlib.target import Target, LinuxTarget, AndroidTarget, LocalLinuxTarget, ChromeOsTarget
 from devlib.host import PACKAGE_BIN_DIRECTORY
-from devlib.exception import DevlibError, TargetError, HostError, TargetNotRespondingError
+from devlib.exception import DevlibError, DevlibTransientError, DevlibStableError, TargetError, TargetTransientError, TargetStableError, TargetNotRespondingError, HostError
 
 from devlib.module import Module, HardRestModule, BootModule, FlashModule
 from devlib.module import get_module, register_module
diff --git a/devlib/exception.py b/devlib/exception.py
index 9a54c4a..0004a4b 100644
--- a/devlib/exception.py
+++ b/devlib/exception.py
@@ -22,12 +22,43 @@ class DevlibError(Exception):
         return str(self)
 
 
+class DevlibStableError(DevlibError):
+    """Non transient target errors, that are not subject to random variations
+    in the environment and can be reliably linked to for example a missing
+    feature on a target."""
+    pass
+
+
+class DevlibTransientError(DevlibError):
+    """Exceptions inheriting from ``DevlibTransientError`` represent random
+    transient events that are usually related to issues in the environment, as
+    opposed to programming errors, for example network failures or
+    timeout-related exceptions. When the error could come from
+    indistinguishable transient or non-transient issue, it can generally be
+    assumed that the configuration is correct and therefore, a transient
+    exception is raised."""
+    pass
+
+
 class TargetError(DevlibError):
     """An error has occured on the target"""
     pass
 
 
-class TargetNotRespondingError(DevlibError):
+class TargetTransientError(TargetError, DevlibTransientError):
+    """Transient target errors that can happen randomly when everything is
+    properly configured."""
+    pass
+
+
+class TargetStableError(TargetError, DevlibStableError):
+    """Non-transient target errors that can be linked to a programming error or
+    a configuration issue, and is not influenced by non-controllable parameters
+    such as network issues."""
+    pass
+
+
+class TargetNotRespondingError(TargetTransientError):
     """The target is unresponsive."""
     pass
 
@@ -38,7 +69,7 @@ class HostError(DevlibError):
 
 
 # pylint: disable=redefined-builtin
-class TimeoutError(DevlibError):
+class TimeoutError(DevlibTransientError):
     """Raised when a subprocess command times out. This is basically a ``DevlibError``-derived version
     of ``subprocess.CalledProcessError``, the thinking being that while a timeout could be due to
     programming error (e.g. not setting long enough timers), it is often due to some failure in the
diff --git a/devlib/host.py b/devlib/host.py
index df3ccc1..f68173c 100644
--- a/devlib/host.py
+++ b/devlib/host.py
@@ -20,7 +20,7 @@ import subprocess
 import logging
 from getpass import getpass
 
-from devlib.exception import TargetError
+from devlib.exception import TargetTransientError, TargetStableError
 from devlib.utils.misc import check_output
 
 PACKAGE_BIN_DIRECTORY = os.path.join(os.path.dirname(__file__), 'bin')
@@ -59,11 +59,11 @@ class LocalConnection(object):
 
     # pylint: disable=unused-argument
     def execute(self, command, timeout=None, check_exit_code=True,
-                as_root=False, strip_colors=True):
+                as_root=False, strip_colors=True, will_succeed=False):
         self.logger.debug(command)
         if as_root:
             if self.unrooted:
-                raise TargetError('unrooted')
+                raise TargetStableError('unrooted')
             password = self._get_password()
             command = 'echo \'{}\' | sudo -S '.format(password) + command
         ignore = None if check_exit_code else 'all'
@@ -72,12 +72,15 @@ class LocalConnection(object):
         except subprocess.CalledProcessError as e:
             message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'.format(
                 e.returncode, command, e.output)
-            raise TargetError(message)
+            if will_succeed:
+                raise TargetTransientError(message)
+            else:
+                raise TargetStableError(message)
 
     def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
         if as_root:
             if self.unrooted:
-                raise TargetError('unrooted')
+                raise TargetStableError('unrooted')
             password = self._get_password()
             command = 'echo \'{}\' | sudo -S '.format(password) + command
         return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True)
diff --git a/devlib/instrument/gem5power.py b/devlib/instrument/gem5power.py
index 4ae901a..35b338b 100644
--- a/devlib/instrument/gem5power.py
+++ b/devlib/instrument/gem5power.py
@@ -16,7 +16,7 @@ from __future__ import division
 
 from devlib.platform.gem5 import Gem5SimulationPlatform
 from devlib.instrument import Instrument, CONTINUOUS, MeasurementsCsv
-from devlib.exception import TargetError
+from devlib.exception import TargetStableError
 from devlib.utils.csvutil import csvwriter
 
 
@@ -36,9 +36,9 @@ class Gem5PowerInstrument(Instrument):
             system.cluster0.cores0.power_model.static_power
         '''
         if not isinstance(target.platform, Gem5SimulationPlatform):
-            raise TargetError('Gem5PowerInstrument requires a gem5 platform')
+            raise TargetStableError('Gem5PowerInstrument requires a gem5 platform')
         if not target.has('gem5stats'):
-            raise TargetError('Gem5StatsModule is not loaded')
+            raise TargetStableError('Gem5StatsModule is not loaded')
         super(Gem5PowerInstrument, self).__init__(target)
 
         # power_sites is assumed to be a list later
diff --git a/devlib/instrument/hwmon.py b/devlib/instrument/hwmon.py
index 72fb2ec..8c7f15d 100644
--- a/devlib/instrument/hwmon.py
+++ b/devlib/instrument/hwmon.py
@@ -16,7 +16,7 @@ from __future__ import division
 import re
 
 from devlib.instrument import Instrument, Measurement, INSTANTANEOUS
-from devlib.exception import TargetError
+from devlib.exception import TargetStableError
 
 
 class HwmonInstrument(Instrument):
@@ -35,7 +35,7 @@ class HwmonInstrument(Instrument):
 
     def __init__(self, target):
         if not hasattr(target, 'hwmon'):
-            raise TargetError('Target does not support HWMON')
+            raise TargetStableError('Target does not support HWMON')
         super(HwmonInstrument, self).__init__(target)
 
         self.logger.debug('Discovering available HWMON sensors...')
diff --git a/devlib/instrument/netstats/__init__.py b/devlib/instrument/netstats/__init__.py
index 797d4a7..8203ceb 100644
--- a/devlib/instrument/netstats/__init__.py
+++ b/devlib/instrument/netstats/__init__.py
@@ -22,7 +22,7 @@ from collections import defaultdict
 from future.moves.itertools import zip_longest
 
 from devlib.instrument import Instrument, MeasurementsCsv, CONTINUOUS
-from devlib.exception import TargetError, HostError
+from devlib.exception import TargetStableError, HostError
 from devlib.utils.android import ApkInfo
 from devlib.utils.csvutil import csvwriter
 
@@ -84,7 +84,7 @@ class NetstatsInstrument(Instrument):
 
         """
         if target.os != 'android':
-            raise TargetError('netstats insturment only supports Android targets')
+            raise TargetStableError('netstats insturment only supports Android targets')
         if apk is None:
             apk = os.path.join(THIS_DIR, 'netstats.apk')
         if not os.path.isfile(apk):
diff --git a/devlib/module/cgroups.py b/devlib/module/cgroups.py
index d60860c..5e442fd 100644
--- a/devlib/module/cgroups.py
+++ b/devlib/module/cgroups.py
@@ -18,7 +18,7 @@ import re
 from collections import namedtuple
 
 from devlib.module import Module
-from devlib.exception import TargetError
+from devlib.exception import TargetStableError
 from devlib.utils.misc import list_to_ranges, isiterable
 from devlib.utils.types import boolean
 
@@ -281,7 +281,7 @@ class CGroup(object):
             self.target.execute('[ -d {0} ]'\
                 .format(self.directory), as_root=True)
             return True
-        except TargetError:
+        except TargetStableError:
             return False
 
     def get(self):
@@ -319,7 +319,7 @@ class CGroup(object):
             # Set the attribute value
             try:
                 self.target.write_value(path, attrs[idx])
-            except TargetError:
+            except TargetStableError:
                 # Check if the error is due to a non-existing attribute
                 attrs = self.get()
                 if idx not in attrs:
@@ -389,9 +389,9 @@ class CgroupsModule(Module):
             controller = Controller(ss.name, hid, hierarchy[hid])
             try:
                 controller.mount(self.target, self.cgroup_root)
-            except TargetError:
+            except TargetStableError:
                 message = 'Failed to mount "{}" controller'
-                raise TargetError(message.format(controller.kind))
+                raise TargetStableError(message.format(controller.kind))
             self.logger.info('  %-12s : %s', controller.kind,
                              controller.mount_point)
             self.controllers[ss.name] = controller
diff --git a/devlib/module/cpufreq.py b/devlib/module/cpufreq.py
index 294d083..1f502da 100644
--- a/devlib/module/cpufreq.py
+++ b/devlib/module/cpufreq.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 #
 from devlib.module import Module
-from devlib.exception import TargetError
+from devlib.exception import TargetStableError
 from devlib.utils.misc import memoized
 
 
@@ -82,7 +82,7 @@ class CpufreqModule(Module):
                Setting the governor on any core in a cluster will also set it on all
                other cores in that cluster.
 
-        :raises: TargetError if governor is not supported by the CPU, or if,
+        :raises: TargetStableError if governor is not supported by the CPU, or if,
                  for some reason, the governor could not be set.
 
         """
@@ -90,7 +90,7 @@ class CpufreqModule(Module):
             cpu = 'cpu{}'.format(cpu)
         supported = self.list_governors(cpu)
         if governor not in supported:
-            raise TargetError('Governor {} not supported for cpu {}'.format(governor, cpu))
+            raise TargetStableError('Governor {} not supported for cpu {}'.format(governor, cpu))
         sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_governor'.format(cpu)
         self.target.write_value(sysfile, governor)
         self.set_governor_tunables(cpu, governor, **kwargs)
@@ -104,11 +104,11 @@ class CpufreqModule(Module):
             try:
                 tunables_path = '/sys/devices/system/cpu/{}/cpufreq/{}'.format(cpu, governor)
                 self._governor_tunables[governor] = self.target.list_directory(tunables_path)
-            except TargetError:  # probably an older kernel
+            except TargetStableError:  # probably an older kernel
                 try:
                     tunables_path = '/sys/devices/system/cpu/cpufreq/{}'.format(governor)
                     self._governor_tunables[governor] = self.target.list_directory(tunables_path)
-                except TargetError:  # governor does not support tunables
+                except TargetStableError:  # governor does not support tunables
                     self._governor_tunables[governor] = []
         return self._governor_tunables[governor]
 
@@ -122,7 +122,7 @@ class CpufreqModule(Module):
                 try:
                     path = '/sys/devices/system/cpu/{}/cpufreq/{}/{}'.format(cpu, governor, tunable)
                     tunables[tunable] = self.target.read_value(path)
-                except TargetError:  # May be an older kernel
+                except TargetStableError:  # May be an older kernel
                     path = '/sys/devices/system/cpu/cpufreq/{}/{}'.format(governor, tunable)
                     tunables[tunable] = self.target.read_value(path)
         return tunables
@@ -140,7 +140,7 @@ class CpufreqModule(Module):
         The rest should be keyword parameters mapping tunable name onto the value to
         be set for it.
 
-        :raises: TargetError if governor specified is not a valid governor name, or if
+        :raises: TargetStableError if governor specified is not a valid governor name, or if
                  a tunable specified is not valid for the governor, or if could not set
                  tunable.
 
@@ -155,7 +155,7 @@ class CpufreqModule(Module):
                 path = '/sys/devices/system/cpu/{}/cpufreq/{}/{}'.format(cpu, governor, tunable)
                 try:
                     self.target.write_value(path, value)
-                except TargetError:
+                except TargetStableError:
                     if self.target.file_exists(path):
                         # File exists but we did something wrong
                         raise
@@ -165,7 +165,7 @@ class CpufreqModule(Module):
             else:
                 message = 'Unexpected tunable {} for governor {} on {}.\n'.format(tunable, governor, cpu)
                 message += 'Available tunables are: {}'.format(valid_tunables)
-                raise TargetError(message)
+                raise TargetStableError(message)
 
     @memoized
     def list_frequencies(self, cpu):
@@ -177,14 +177,14 @@ class CpufreqModule(Module):
             cmd = 'cat /sys/devices/system/cpu/{}/cpufreq/scaling_available_frequencies'.format(cpu)
             output = self.target.execute(cmd)
             available_frequencies = list(map(int, output.strip().split()))  # pylint: disable=E1103
-        except TargetError:
+        except TargetStableError:
             # On some devices scaling_frequencies  is not generated.
             # http://adrynalyne-teachtofish.blogspot.co.uk/2011/11/how-to-enable-scalingavailablefrequenci.html
             # Fall back to parsing stats/time_in_state
             path = '/sys/devices/system/cpu/{}/cpufreq/stats/time_in_state'.format(cpu)
             try:
                 out_iter = iter(self.target.read_value(path).split())
-            except TargetError:
+            except TargetStableError:
                 if not self.target.file_exists(path):
                     # Probably intel_pstate. Can't get available freqs.
                     return []
@@ -219,7 +219,7 @@ class CpufreqModule(Module):
         try to read the minimum frequency and the following exception will be
         raised ::
 
-        :raises: TargetError if for some reason the frequency could not be read.
+        :raises: TargetStableError if for some reason the frequency could not be read.
 
         """
         if isinstance(cpu, int):
@@ -239,7 +239,7 @@ class CpufreqModule(Module):
 
         on the device.
 
-        :raises: TargetError if the frequency is not supported by the CPU, or if, for
+        :raises: TargetStableError if the frequency is not supported by the CPU, or if, for
                  some reason, frequency could not be set.
         :raises: ValueError if ``frequency`` is not an integer.
 
@@ -250,7 +250,7 @@ class CpufreqModule(Module):
         try:
             value = int(frequency)
             if exact and available_frequencies and value not in available_frequencies:
-                raise TargetError('Can\'t set {} frequency to {}\nmust be in {}'.format(cpu,
+                raise TargetStableError('Can\'t set {} frequency to {}\nmust be in {}'.format(cpu,
                                                                                         value,
                                                                                         available_frequencies))
             sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_min_freq'.format(cpu)
@@ -266,7 +266,7 @@ class CpufreqModule(Module):
         try to read the current frequency and the following exception will be
         raised ::
 
-        :raises: TargetError if for some reason the frequency could not be read.
+        :raises: TargetStableError if for some reason the frequency could not be read.
 
         """
         if isinstance(cpu, int):
@@ -288,7 +288,7 @@ class CpufreqModule(Module):
 
         on the device (if it exists).
 
-        :raises: TargetError if the frequency is not supported by the CPU, or if, for
+        :raises: TargetStableError if the frequency is not supported by the CPU, or if, for
                  some reason, frequency could not be set.
         :raises: ValueError if ``frequency`` is not an integer.
 
@@ -300,11 +300,11 @@ class CpufreqModule(Module):
             if exact:
                 available_frequencies = self.list_frequencies(cpu)
                 if available_frequencies and value not in available_frequencies:
-                    raise TargetError('Can\'t set {} frequency to {}\nmust be in {}'.format(cpu,
+                    raise TargetStableError('Can\'t set {} frequency to {}\nmust be in {}'.format(cpu,
                                                                                             value,
                                                                                             available_frequencies))
             if self.get_governor(cpu) != 'userspace':
-                raise TargetError('Can\'t set {} frequency; governor must be "userspace"'.format(cpu))
+                raise TargetStableError('Can\'t set {} frequency; governor must be "userspace"'.format(cpu))
             sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_setspeed'.format(cpu)
             self.target.write_value(sysfile, value, verify=False)
         except ValueError:
@@ -318,7 +318,7 @@ class CpufreqModule(Module):
         try to read the maximum frequency and the following exception will be
         raised ::
 
-        :raises: TargetError if for some reason the frequency could not be read.
+        :raises: TargetStableError if for some reason the frequency could not be read.
         """
         if isinstance(cpu, int):
             cpu = 'cpu{}'.format(cpu)
@@ -337,7 +337,7 @@ class CpufreqModule(Module):
 
         on the device.
 
-        :raises: TargetError if the frequency is not supported by the CPU, or if, for
+        :raises: TargetStableError if the frequency is not supported by the CPU, or if, for
                  some reason, frequency could not be set.
         :raises: ValueError if ``frequency`` is not an integer.
 
@@ -348,7 +348,7 @@ class CpufreqModule(Module):
         try:
             value = int(frequency)
             if exact and available_frequencies and value not in available_frequencies:
-                raise TargetError('Can\'t set {} frequency to {}\nmust be in {}'.format(cpu,
+                raise TargetStableError('Can\'t set {} frequency to {}\nmust be in {}'.format(cpu,
                                                                                         value,
                                                                                         available_frequencies))
             sysfile = '/sys/devices/system/cpu/{}/cpufreq/scaling_max_freq'.format(cpu)
@@ -409,13 +409,13 @@ class CpufreqModule(Module):
             return self.target._execute_util(
                 'cpufreq_set_all_governors {}'.format(governor),
                 as_root=True)
-        except TargetError as e:
+        except TargetStableError as e:
             if ("echo: I/O error" in str(e) or
                 "write error: Invalid argument" in str(e)):
 
                 cpus_unsupported = [c for c in self.target.list_online_cpus()
                                     if governor not in self.list_governors(c)]
-                raise TargetError("Governor {} unsupported for CPUs {}".format(
+                raise TargetStableError("Governor {} unsupported for CPUs {}".format(
                     governor, cpus_unsupported))
             else:
                 raise
diff --git a/devlib/module/devfreq.py b/devlib/module/devfreq.py
index 8c5d885..00c3154 100644
--- a/devlib/module/devfreq.py
+++ b/devlib/module/devfreq.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 #
 from devlib.module import Module
-from devlib.exception import TargetError
+from devlib.exception import TargetStableError
 from devlib.utils.misc import memoized
 
 class DevfreqModule(Module):
@@ -64,13 +64,13 @@ class DevfreqModule(Module):
         Additional keyword arguments can be used to specify governor tunables for
         governors that support them.
 
-        :raises: TargetError if governor is not supported by the device, or if,
+        :raises: TargetStableError if governor is not supported by the device, or if,
                  for some reason, the governor could not be set.
 
         """
         supported = self.list_governors(device)
         if governor not in supported:
-            raise TargetError('Governor {} not supported for device {}'.format(governor, device))
+            raise TargetStableError('Governor {} not supported for device {}'.format(governor, device))
         sysfile = '/sys/class/devfreq/{}/governor'.format(device)
         self.target.write_value(sysfile, governor)
 
@@ -94,7 +94,7 @@ class DevfreqModule(Module):
         will try to read the minimum frequency and the following exception will
         be raised ::
 
-        :raises: TargetError if for some reason the frequency could not be read.
+        :raises: TargetStableError if for some reason the frequency could not be read.
 
         """
         sysfile = '/sys/class/devfreq/{}/min_freq'.format(device)
@@ -112,7 +112,7 @@ class DevfreqModule(Module):
 
         on the device.
 
-        :raises: TargetError if the frequency is not supported by the device, or if, for
+        :raises: TargetStableError if the frequency is not supported by the device, or if, for
                  some reason, frequency could not be set.
         :raises: ValueError if ``frequency`` is not an integer.
 
@@ -121,7 +121,7 @@ class DevfreqModule(Module):
         try:
             value = int(frequency)
             if exact and available_frequencies and value not in available_frequencies:
-                raise TargetError('Can\'t set {} frequency to {}\nmust be in {}'.format(device,
+                raise TargetStableError('Can\'t set {} frequency to {}\nmust be in {}'.format(device,
                                                                                         value,
                                                                                         available_frequencies))
             sysfile = '/sys/class/devfreq/{}/min_freq'.format(device)
@@ -137,7 +137,7 @@ class DevfreqModule(Module):
         will try to read the current frequency and the following exception will
         be raised ::
 
-        :raises: TargetError if for some reason the frequency could not be read.
+        :raises: TargetStableError if for some reason the frequency could not be read.
 
         """
         sysfile = '/sys/class/devfreq/{}/cur_freq'.format(device)
@@ -151,7 +151,7 @@ class DevfreqModule(Module):
         try to read the maximum frequency and the following exception will be
         raised ::
 
-        :raises: TargetError if for some reason the frequency could not be read.
+        :raises: TargetStableError if for some reason the frequency could not be read.
         """
         sysfile = '/sys/class/devfreq/{}/max_freq'.format(device)
         return self.target.read_int(sysfile)
@@ -168,7 +168,7 @@ class DevfreqModule(Module):
 
         on the device.
 
-        :raises: TargetError if the frequency is not supported by the device, or
+        :raises: TargetStableError if the frequency is not supported by the device, or
                  if, for some reason, frequency could not be set.
         :raises: ValueError if ``frequency`` is not an integer.
 
@@ -180,7 +180,7 @@ class DevfreqModule(Module):
             raise ValueError('Frequency must be an integer; got: "{}"'.format(frequency))
 
         if exact and value not in available_frequencies:
-            raise TargetError('Can\'t set {} frequency to {}\nmust be in {}'.format(device,
+            raise TargetStableError('Can\'t set {} frequency to {}\nmust be in {}'.format(device,
                                                                                     value,
                                                                                     available_frequencies))
         sysfile = '/sys/class/devfreq/{}/max_freq'.format(device)
@@ -202,13 +202,13 @@ class DevfreqModule(Module):
         try:
             return self.target._execute_util(  # pylint: disable=protected-access
                 'devfreq_set_all_governors {}'.format(governor), as_root=True)
-        except TargetError as e:
+        except TargetStableError as e:
             if ("echo: I/O error" in str(e) or
                 "write error: Invalid argument" in str(e)):
 
                 devs_unsupported = [d for d in self.target.list_devices()
                                     if governor not in self.list_governors(d)]
-                raise TargetError("Governor {} unsupported for devices {}".format(
+                raise TargetStableError("Governor {} unsupported for devices {}".format(
                     governor, devs_unsupported))
             else:
                 raise
diff --git a/devlib/module/gem5stats.py b/devlib/module/gem5stats.py
index 29b1858..eef848d 100644
--- a/devlib/module/gem5stats.py
+++ b/devlib/module/gem5stats.py
@@ -17,7 +17,7 @@ import sys
 import os.path
 from collections import defaultdict
 
-from devlib.exception import TargetError, HostError
+from devlib.exception import TargetStableError, HostError
 from devlib.module import Module
 from devlib.platform.gem5 import Gem5SimulationPlatform
 from devlib.utils.gem5 import iter_statistics_dump, GEM5STATS_ROI_NUMBER
@@ -87,13 +87,13 @@ class Gem5StatsModule(Module):
         if label not in self.rois:
             raise KeyError('Incorrect ROI label: {}'.format(label))
         if not self.rois[label].start():
-            raise TargetError('ROI {} was already running'.format(label))
+            raise TargetStableError('ROI {} was already running'.format(label))
 
     def roi_end(self, label):
         if label not in self.rois:
             raise KeyError('Incorrect ROI label: {}'.format(label))
         if not self.rois[label].stop():
-            raise TargetError('ROI {} was not running'.format(label))
+            raise TargetStableError('ROI {} was not running'.format(label))
 
     def start_periodic_dump(self, delay_ns=0, period_ns=10000000):
         # Default period is 10ms because it's roughly what's needed to have
diff --git a/devlib/module/gpufreq.py b/devlib/module/gpufreq.py
index 1cccb27..9f0a952 100644
--- a/devlib/module/gpufreq.py
+++ b/devlib/module/gpufreq.py
@@ -29,7 +29,7 @@
 
 import re
 from devlib.module import Module
-from devlib.exception import TargetError
+from devlib.exception import TargetStableError
 from devlib.utils.misc import memoized
 
 class GpufreqModule(Module):
@@ -56,7 +56,7 @@ class GpufreqModule(Module):
 
     def set_governor(self, governor):
         if governor not in self.governors:
-            raise TargetError('Governor {} not supported for gpu'.format(governor))
+            raise TargetStableError('Governor {} not supported for gpu'.format(governor))
         self.target.write_value("/sys/kernel/gpu/gpu_governor", governor)
 
     def get_frequencies(self):
@@ -73,7 +73,7 @@ class GpufreqModule(Module):
         try to read the current frequency and the following exception will be
         raised ::
 
-        :raises: TargetError if for some reason the frequency could not be read.
+        :raises: TargetStableError if for some reason the frequency could not be read.
 
         """
         return int(self.target.read_value("/sys/kernel/gpu/gpu_clock"))
diff --git a/devlib/module/hwmon.py b/devlib/module/hwmon.py
index 9c037e8..7145ae5 100644
--- a/devlib/module/hwmon.py
+++ b/devlib/module/hwmon.py
@@ -15,7 +15,7 @@
 import re
 from collections import defaultdict
 
-from devlib import TargetError
+from devlib import TargetStableError
 from devlib.module import Module
 from devlib.utils.types import integer
 
@@ -118,7 +118,7 @@ class HwmonModule(Module):
     def probe(target):
         try:
             target.list_directory(HWMON_ROOT, as_root=target.is_rooted)
-        except TargetError:
+        except TargetStableError:
             # Doesn't exist or no permissions
             return False
         return True
diff --git a/devlib/module/vexpress.py b/devlib/module/vexpress.py
index 8f22cf7..0f8ab97 100644
--- a/devlib/module/vexpress.py
+++ b/devlib/module/vexpress.py
@@ -20,7 +20,7 @@ import shutil
 from subprocess import CalledProcessError
 
 from devlib.module import HardRestModule, BootModule, FlashModule
-from devlib.exception import TargetError, HostError
+from devlib.exception import TargetError, TargetStableError, HostError
 from devlib.utils.serial_port import open_serial_connection, pulse_dtr, write_characters
 from devlib.utils.uefi import UefiMenu, UefiConfig
 from devlib.utils.uboot import UbootMenu
@@ -89,7 +89,7 @@ class VexpressReboottxtHardReset(HardRestModule):
         try:
             if self.target.is_connected:
                 self.target.execute('sync')
-        except TargetError:
+        except (TargetError, CalledProcessError):
             pass
 
         if not os.path.exists(self.path):
@@ -225,7 +225,7 @@ class VexpressUefiShellBoot(VexpressBootModule):
         try:
             menu.select(self.uefi_entry)
         except LookupError:
-            raise TargetError('Did not see "{}" UEFI entry.'.format(self.uefi_entry))
+            raise TargetStableError('Did not see "{}" UEFI entry.'.format(self.uefi_entry))
         tty.expect(self.efi_shell_prompt, timeout=self.timeout)
         if self.bootargs:
             tty.sendline('')  # stop default boot
@@ -344,7 +344,7 @@ class VersatileExpressFlashModule(FlashModule):
             os.system('sync')
         except (IOError, OSError) as e:
             msg = 'Could not deploy images to {}; got: {}'
-            raise TargetError(msg.format(self.vemsd_mount, e))
+            raise TargetStableError(msg.format(self.vemsd_mount, e))
         self.target.boot()
         self.target.connect(timeout=30)
 
@@ -390,4 +390,4 @@ def wait_for_vemsd(vemsd_mount, tty, mcc_prompt=DEFAULT_MCC_PROMPT, short_delay=
         time.sleep(short_delay * 3)
         if os.path.exists(path):
             return
-    raise TargetError('Could not mount {}'.format(vemsd_mount))
+    raise TargetStableError('Could not mount {}'.format(vemsd_mount))
diff --git a/devlib/platform/arm.py b/devlib/platform/arm.py
index ae0f1d9..4dbf98f 100644
--- a/devlib/platform/arm.py
+++ b/devlib/platform/arm.py
@@ -19,7 +19,7 @@ import tempfile
 import time
 import pexpect
 
-from devlib.exception import TargetError, HostError
+from devlib.exception import TargetStableError, HostError
 from devlib.host import PACKAGE_BIN_DIRECTORY
 from devlib.instrument import (Instrument, InstrumentChannel, MeasurementsCsv,
                                Measurement, CONTINUOUS, INSTANTANEOUS)
@@ -123,7 +123,7 @@ class VersatileExpressPlatform(Platform):
                     except pexpect.TIMEOUT:
                         pass  # We have our own timeout -- see below.
                     if (time.time() - wait_start_time) > self.ready_timeout:
-                        raise TargetError('Could not acquire IP address.')
+                        raise TargetTransientError('Could not acquire IP address.')
             finally:
                 tty.sendline('exit')  # exit shell created by "su" call at the start
 
diff --git a/devlib/platform/gem5.py b/devlib/platform/gem5.py
index 5b067dc..817699a 100644
--- a/devlib/platform/gem5.py
+++ b/devlib/platform/gem5.py
@@ -19,7 +19,7 @@ import shutil
 import time
 import types
 
-from devlib.exception import TargetError
+from devlib.exception import TargetStableError
 from devlib.host import PACKAGE_BIN_DIRECTORY
 from devlib.platform import Platform
 from devlib.utils.ssh import AndroidGem5Connection, LinuxGem5Connection
@@ -86,12 +86,12 @@ class Gem5SimulationPlatform(Platform):
         Check if the command to start gem5 makes sense
         """
         if self.gem5args_binary is None:
-            raise TargetError('Please specify a gem5 binary.')
+            raise TargetStableError('Please specify a gem5 binary.')
         if self.gem5args_args is None:
-            raise TargetError('Please specify the arguments passed on to gem5.')
+            raise TargetStableError('Please specify the arguments passed on to gem5.')
         self.gem5args_virtio = str(self.gem5args_virtio).format(self.gem5_interact_dir)
         if self.gem5args_virtio is None:
-            raise TargetError('Please specify arguments needed for virtIO.')
+            raise TargetStableError('Please specify arguments needed for virtIO.')
 
     def _start_interaction_gem5(self):
         """
@@ -110,7 +110,7 @@ class Gem5SimulationPlatform(Platform):
             if not os.path.exists(self.stats_directory):
                 os.mkdir(self.stats_directory)
             if os.path.exists(self.gem5_out_dir):
-                raise TargetError("The gem5 stats directory {} already "
+                raise TargetStableError("The gem5 stats directory {} already "
                                   "exists.".format(self.gem5_out_dir))
             else:
                 os.mkdir(self.gem5_out_dir)
@@ -153,7 +153,7 @@ class Gem5SimulationPlatform(Platform):
         e.g. pid, input directory etc
         """
         self.logger("This functionality is not yet implemented")
-        raise TargetError()
+        raise TargetStableError()
 
     def _intercept_telnet_port(self):
         """
@@ -161,13 +161,13 @@ class Gem5SimulationPlatform(Platform):
         """
 
         if self.gem5 is None:
-            raise TargetError('The platform has no gem5 simulation! '
+            raise TargetStableError('The platform has no gem5 simulation! '
                               'Something went wrong')
         while self.gem5_port is None:
             # Check that gem5 is running!
             if self.gem5.poll():
                 message = "The gem5 process has crashed with error code {}!\n\tPlease see {} for details."
-                raise TargetError(message.format(self.gem5.poll(), self.stderr_file.name))
+                raise TargetStableError(message.format(self.gem5.poll(), self.stderr_file.name))
 
             # Open the stderr file
             with open(self.stderr_filename, 'r') as f:
@@ -185,7 +185,7 @@ class Gem5SimulationPlatform(Platform):
                     # Check if the sockets are not disabled
                     m = re.search(r"Sockets disabled, not accepting terminal connections", line)
                     if m:
-                        raise TargetError("The sockets have been disabled!"
+                        raise TargetStableError("The sockets have been disabled!"
                                           "Pass --listener-mode=on to gem5")
                 else:
                     time.sleep(1)
@@ -287,10 +287,10 @@ class Gem5SimulationPlatform(Platform):
 
 # Methods that will be monkey-patched onto the target
 def _overwritten_reset(self):  # pylint: disable=unused-argument
-    raise TargetError('Resetting is not allowed on gem5 platforms!')
+    raise TargetStableError('Resetting is not allowed on gem5 platforms!')
 
 def _overwritten_reboot(self):  # pylint: disable=unused-argument
-    raise TargetError('Rebooting is not allowed on gem5 platforms!')
+    raise TargetStableError('Rebooting is not allowed on gem5 platforms!')
 
 def _overwritten_capture_screen(self, filepath):
     connection_screencapped = self.platform.gem5_capture_screen(filepath)
diff --git a/devlib/target.py b/devlib/target.py
index 44275d1..5c4cfd2 100644
--- a/devlib/target.py
+++ b/devlib/target.py
@@ -29,7 +29,7 @@ from collections import namedtuple
 from devlib.host import LocalConnection, PACKAGE_BIN_DIRECTORY
 from devlib.module import get_module
 from devlib.platform import Platform
-from devlib.exception import TargetError, TargetNotRespondingError, TimeoutError  # pylint: disable=redefined-builtin
+from devlib.exception import DevlibTransientError, TargetStableError, TargetNotRespondingError, TimeoutError # pylint: disable=redefined-builtin
 from devlib.utils.ssh import SshConnection
 from devlib.utils.android import AdbConnection, AndroidProperties, LogcatMonitor, adb_command, adb_disconnect, INTENT_FLAGS
 from devlib.utils.misc import memoized, isiterable, convert_new_lines
@@ -104,7 +104,7 @@ class Target(object):
         try:
             self.execute('ls /', timeout=5, as_root=True)
             return True
-        except (TargetError, TimeoutError):
+        except (TargetStableError, TimeoutError):
             return False
 
     @property
@@ -150,11 +150,11 @@ class Target(object):
     def config(self):
         try:
             return KernelConfig(self.execute('zcat /proc/config.gz'))
-        except TargetError:
+        except TargetStableError:
             for path in ['/boot/config', '/boot/config-$(uname -r)']:
                 try:
                     return KernelConfig(self.execute('cat {}'.format(path)))
-                except TargetError:
+                except TargetStableError:
                     pass
         return KernelConfig('')
 
@@ -203,7 +203,7 @@ class Target(object):
         # Check if the user hasn't given two different platforms
         if 'platform' in self.connection_settings:
             if connection_settings['platform'] is not platform:
-                raise TargetError('Platform specified in connection_settings '
+                raise TargetStableError('Platform specified in connection_settings '
                                    '({}) differs from that directly passed '
                                    '({})!)'
                                    .format(connection_settings['platform'],
@@ -282,14 +282,14 @@ class Target(object):
     def reboot(self, hard=False, connect=True, timeout=180):
         if hard:
             if not self.has('hard_reset'):
-                raise TargetError('Hard reset not supported for this target.')
+                raise TargetStableError('Hard reset not supported for this target.')
             self.hard_reset()  # pylint: disable=no-member
         else:
             if not self.is_connected:
                 message = 'Cannot reboot target becuase it is disconnected. ' +\
                           'Either connect() first, or specify hard=True ' +\
                           '(in which case, a hard_reset module must be installed)'
-                raise TargetError(message)
+                raise TargetTransientError(message)
             self.reset()
             # Wait a fixed delay before starting polling to give the target time to
             # shut down, otherwise, might create the connection while it's still shutting
@@ -346,7 +346,7 @@ class Target(object):
         try:
             self.execute('{} tar -cvf {} {}'.format(self.busybox, tar_file_name,
                                                      source_dir), as_root=as_root)
-        except TargetError:
+        except TargetStableError:
             self.logger.debug('Failed to run tar command on target! ' \
                               'Not pulling directory {}'.format(source_dir))
         # Pull the file
@@ -360,8 +360,10 @@ class Target(object):
 
     # execution
 
-    def execute(self, command, timeout=None, check_exit_code=True, as_root=False):
-        return self.conn.execute(command, timeout, check_exit_code, as_root)
+    def execute(self, command, timeout=None, check_exit_code=True,
+                as_root=False, will_succeed=False):
+        return self.conn.execute(command, timeout, check_exit_code, as_root,
+                will_succeed)
 
     def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
         return self.conn.background(command, stdout, stderr, as_root)
@@ -459,12 +461,12 @@ class Target(object):
             output = self.read_value(path)
             if not output == value:
                 message = 'Could not set the value of {} to "{}" (read "{}")'.format(path, value, output)
-                raise TargetError(message)
+                raise TargetStableError(message)
 
     def reset(self):
         try:
             self.execute('reboot', as_root=self.needs_su, timeout=2)
-        except (TargetError, TimeoutError, subprocess.CalledProcessError):
+        except (DevlibTransientError, subprocess.CalledProcessError):
             # on some targets "reboot" doesn't return gracefully
             pass
         self._connected_as_root = None
@@ -473,7 +475,7 @@ class Target(object):
         try:
             self.conn.execute('ls /', timeout=5)
             return 1
-        except (TimeoutError, subprocess.CalledProcessError, TargetError):
+        except (DevlibTransientError, subprocess.CalledProcessError):
             if explode:
                 raise TargetNotRespondingError('Target {} is not responding'.format(self.conn.name))
             return 0
@@ -488,7 +490,7 @@ class Target(object):
         for pid in self.get_pids_of(process_name):
             try:
                 self.kill(pid, signal=signal, as_root=as_root)
-            except TargetError:
+            except TargetStableError:
                 pass
 
     def get_pids_of(self, process_name):
@@ -588,7 +590,7 @@ class Target(object):
                 try:
                     if name in self.list_directory(path):
                         return self.path.join(path, name)
-                except TargetError:
+                except TargetStableError:
                     pass  # directory does not exist or no executable permissions
 
     which = get_installed
@@ -779,7 +781,7 @@ class Target(object):
             try:
                 self.execute(command)
                 return True
-            except TargetError as e:
+            except TargetStableError as e:
                 err = str(e).lower()
                 if '100% packet loss' in err:
                     # We sent a packet but got no response.
@@ -823,15 +825,12 @@ class LinuxTarget(Target):
     @memoized
     def os_version(self):
         os_version = {}
-        try:
-            command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
-            version_files = self.execute(command, check_exit_code=False).strip().split()
-            for vf in version_files:
-                name = self.path.basename(vf)
-                output = self.read_value(vf)
-                os_version[name] = convert_new_lines(output.strip()).replace('\n', ' ')
-        except TargetError:
-            raise
+        command = 'ls /etc/*-release /etc*-version /etc/*_release /etc/*_version 2>/dev/null'
+        version_files = self.execute(command, check_exit_code=False).strip().split()
+        for vf in version_files:
+            name = self.path.basename(vf)
+            output = self.read_value(vf)
+            os_version[name] = convert_new_lines(output.strip()).replace('\n', ' ')
         return os_version
 
     @property
@@ -938,7 +937,7 @@ class LinuxTarget(Target):
             filepath = filepath.format(ts=ts)
             self.pull(tmpfile, filepath)
             self.remove(tmpfile)
-        except TargetError as e:
+        except TargetStableError as e:
             if "Can't open X dispay." not in e.message:
                 raise e
             message = e.message.split('OUTPUT:', 1)[1].strip()  # pylint: disable=no-member
@@ -1079,7 +1078,7 @@ class AndroidTarget(Target):
         try:
             self.execute('reboot {}'.format(fastboot and 'fastboot' or ''),
                          as_root=self.needs_su, timeout=2)
-        except (TargetError, TimeoutError, subprocess.CalledProcessError):
+        except (DevlibTransientError, subprocess.CalledProcessError):
             # on some targets "reboot" doesn't return gracefully
             pass
         self._connected_as_root = None
@@ -1091,7 +1090,9 @@ class AndroidTarget(Target):
             time.sleep(5)
             boot_completed = boolean(self.getprop('sys.boot_completed'))
         if not boot_completed:
-            raise TargetError('Connected but Android did not fully boot.')
+            # Raise a TargetStableError as this usually happens because of
+            # an issue with Android more than a timeout that is too small.
+            raise TargetStableError('Connected but Android did not fully boot.')
 
     def connect(self, timeout=30, check_boot_completed=True):  # pylint: disable=arguments-differ
         device = self.connection_settings.get('device')
@@ -1128,7 +1129,7 @@ class AndroidTarget(Target):
         self.ls_command = 'ls -1'
         try:
             self.execute('ls -1 {}'.format(self.working_directory), as_root=False)
-        except TargetError:
+        except TargetStableError:
             self.ls_command = 'ls'
 
     def list_directory(self, path, as_root=False):
@@ -1240,7 +1241,7 @@ class AndroidTarget(Target):
             swipe_height = height * 2 // 3
             self.input_swipe(swipe_middle, swipe_height, swipe_middle, 0)
         else:
-            raise TargetError("Invalid swipe direction: {}".format(direction))
+            raise TargetStableError("Invalid swipe direction: {}".format(direction))
 
     def getprop(self, prop=None):
         props = AndroidProperties(self.execute('getprop'))
@@ -1307,12 +1308,12 @@ class AndroidTarget(Target):
             self.logger.debug("Replace APK = {}, ADB flags = '{}'".format(replace, ' '.join(flags)))
             return adb_command(self.adb_name, "install {} '{}'".format(' '.join(flags), filepath), timeout=timeout)
         else:
-            raise TargetError('Can\'t install {}: unsupported format.'.format(filepath))
+            raise TargetStableError('Can\'t install {}: unsupported format.'.format(filepath))
 
     def grant_package_permission(self, package, permission):
         try:
             return self.execute('pm grant {} {}'.format(package, permission))
-        except TargetError as e:
+        except TargetStableError as e:
             if 'is not a changeable permission type' in e.message:
                 pass # Ignore if unchangeable
             elif 'Unknown permission' in e.message:
@@ -1411,7 +1412,7 @@ class AndroidTarget(Target):
         if match:
             return boolean(match.group(1))
         else:
-            raise TargetError('Could not establish screen state.')
+            raise TargetStableError('Could not establish screen state.')
 
     def ensure_screen_is_on(self):
         if not self.is_screen_on():
@@ -1456,7 +1457,7 @@ class AndroidTarget(Target):
     def set_airplane_mode(self, mode):
         root_required = self.get_sdk_version() > 23
         if root_required and not self.is_rooted:
-            raise TargetError('Root is required to toggle airplane mode on Android 7+')
+            raise TargetStableError('Root is required to toggle airplane mode on Android 7+')
         mode = int(boolean(mode))
         cmd = 'settings put global airplane_mode_on {}'
         self.execute(cmd.format(mode))
@@ -1540,7 +1541,7 @@ class AndroidTarget(Target):
                              as_root=True)
         else:
             message = 'Could not find mount point for executables directory {}'
-            raise TargetError(message.format(self.executables_directory))
+            raise TargetStableError(message.format(self.executables_directory))
 
     _charging_enabled_path = '/sys/class/power_supply/battery/charging_enabled'
 
diff --git a/devlib/trace/ftrace.py b/devlib/trace/ftrace.py
index 9f08985..8fef36c 100644
--- a/devlib/trace/ftrace.py
+++ b/devlib/trace/ftrace.py
@@ -23,7 +23,7 @@ import sys
 
 from devlib.trace import TraceCollector
 from devlib.host import PACKAGE_BIN_DIRECTORY
-from devlib.exception import TargetError, HostError
+from devlib.exception import TargetStableError, HostError
 from devlib.utils.misc import check_output, which
 
 
@@ -99,7 +99,7 @@ class FtraceCollector(TraceCollector):
         self.kernelshark = which('kernelshark')
 
         if not self.target.is_rooted:
-            raise TargetError('trace-cmd instrument cannot be used on an unrooted device.')
+            raise TargetStableError('trace-cmd instrument cannot be used on an unrooted device.')
         if self.autoreport and not self.report_on_target and self.host_binary is None:
             raise HostError('trace-cmd binary must be installed on the host if autoreport=True.')
         if self.autoview and self.kernelshark is None:
@@ -109,7 +109,7 @@ class FtraceCollector(TraceCollector):
             self.target_binary = self.target.install(host_file)
         else:
             if not self.target.is_installed('trace-cmd'):
-                raise TargetError('No trace-cmd found on device and no_install=True is specified.')
+                raise TargetStableError('No trace-cmd found on device and no_install=True is specified.')
             self.target_binary = 'trace-cmd'
 
         # Validate required events to be traced
@@ -127,7 +127,7 @@ class FtraceCollector(TraceCollector):
             if not list(filter(event_re.match, available_events)):
                 message = 'Event [{}] not available for tracing'.format(event)
                 if strict:
-                    raise TargetError(message)
+                    raise TargetStableError(message)
                 self.target.logger.warning(message)
             else:
                 selected_events.append(event)
@@ -142,7 +142,7 @@ class FtraceCollector(TraceCollector):
         # Check for function tracing support
         if self.functions:
             if not self.target.file_exists(self.function_profile_file):
-                raise TargetError('Function profiling not supported. '\
+                raise TargetStableError('Function profiling not supported. '\
                         'A kernel build with CONFIG_FUNCTION_PROFILER enable is required')
             # Validate required functions to be traced
             available_functions = self.target.execute(
@@ -153,7 +153,7 @@ class FtraceCollector(TraceCollector):
                 if function not in available_functions:
                     message = 'Function [{}] not available for profiling'.format(function)
                     if strict:
-                        raise TargetError(message)
+                        raise TargetStableError(message)
                     self.target.logger.warning(message)
                 else:
                     selected_functions.append(function)
@@ -283,7 +283,7 @@ class FtraceCollector(TraceCollector):
             if sys.version_info[0] == 3:
                 error = error.decode(sys.stdout.encoding, 'replace')
             if process.returncode:
-                raise TargetError('trace-cmd returned non-zero exit code {}'.format(process.returncode))
+                raise TargetStableError('trace-cmd returned non-zero exit code {}'.format(process.returncode))
             if error:
                 # logged at debug level, as trace-cmd always outputs some
                 # errors that seem benign.
diff --git a/devlib/trace/systrace.py b/devlib/trace/systrace.py
index c150a96..2f889c0 100644
--- a/devlib/trace/systrace.py
+++ b/devlib/trace/systrace.py
@@ -34,7 +34,7 @@ import subprocess
 from shutil import copyfile
 from tempfile import NamedTemporaryFile
 
-from devlib.exception import TargetError, HostError
+from devlib.exception import TargetStableError, HostError
 from devlib.trace import TraceCollector
 from devlib.utils.android import platform_tools
 from devlib.utils.misc import memoized
@@ -109,12 +109,12 @@ class SystraceCollector(TraceCollector):
             if category not in self.available_categories:
                 message = 'Category [{}] not available for tracing'.format(category)
                 if strict:
-                    raise TargetError(message)
+                    raise TargetStableError(message)
                 self.logger.warning(message)
 
         self.categories = list(set(self.categories) & set(self.available_categories))
         if not self.categories:
-            raise TargetError('None of the requested categories are available')
+            raise TargetStableError('None of the requested categories are available')
 
     def __del__(self):
         self.reset()
diff --git a/devlib/utils/android.py b/devlib/utils/android.py
index 029c6d4..604d820 100755
--- a/devlib/utils/android.py
+++ b/devlib/utils/android.py
@@ -29,7 +29,7 @@ import subprocess
 from collections import defaultdict
 import pexpect
 
-from devlib.exception import TargetError, HostError
+from devlib.exception import TargetTransientError, TargetStableError, HostError, DevlibError
 from devlib.utils.misc import check_output, which, ABI_MAP
 from devlib.utils.misc import escape_single_quotes, escape_double_quotes
 
@@ -257,9 +257,15 @@ class AdbConnection(object):
 
     # pylint: disable=unused-argument
     def execute(self, command, timeout=None, check_exit_code=False,
-                as_root=False, strip_colors=True):
-        return adb_shell(self.device, command, timeout, check_exit_code,
-                         as_root, adb_server=self.adb_server)
+                as_root=False, strip_colors=True, will_succeed=False):
+        try:
+            return adb_shell(self.device, command, timeout, check_exit_code,
+                             as_root, adb_server=self.adb_server)
+        except TargetStableError as e:
+            if will_succeed:
+                raise TargetTransientError(e)
+            else:
+                raise
 
     def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
         return adb_background_shell(self.device, command, stdout, stderr, as_root)
@@ -357,7 +363,7 @@ def adb_disconnect(device):
         logger.debug(command)
         retval = subprocess.call(command, stdout=open(os.devnull, 'wb'), shell=True)
         if retval:
-            raise TargetError('"{}" returned {}'.format(command, retval))
+            raise TargetTransientError('"{}" returned {}'.format(command, retval))
 
 
 def _ping(device):
@@ -394,7 +400,7 @@ def adb_shell(device, command, timeout=None, check_exit_code=False,
     try:
         raw_output, _ = check_output(actual_command, timeout, shell=False, combined_output=True)
     except subprocess.CalledProcessError as e:
-        raise TargetError(str(e))
+        raise TargetStableError(str(e))
 
     if raw_output:
         try:
@@ -413,19 +419,19 @@ def adb_shell(device, command, timeout=None, check_exit_code=False,
             if int(exit_code):
                 message = ('Got exit code {}\nfrom target command: {}\n'
                            'OUTPUT: {}')
-                raise TargetError(message.format(exit_code, command, output))
+                raise TargetStableError(message.format(exit_code, command, output))
             elif re_search:
                 message = 'Could not start activity; got the following:\n{}'
-                raise TargetError(message.format(re_search[0]))
+                raise TargetStableError(message.format(re_search[0]))
         else:  # not all digits
             if re_search:
                 message = 'Could not start activity; got the following:\n{}'
-                raise TargetError(message.format(re_search[0]))
+                raise TargetStableError(message.format(re_search[0]))
             else:
                 message = 'adb has returned early; did not get an exit code. '\
                           'Was kill-server invoked?\nOUTPUT:\n-----\n{}\n'\
                           '-----'
-                raise TargetError(message.format(raw_output))
+                raise TargetTransientError(message.format(raw_output))
 
     return output
 
@@ -484,7 +490,7 @@ def grant_app_permissions(target, package):
     for permission in permissions:
         try:
             target.execute('pm grant {} {}'.format(package, permission))
-        except TargetError:
+        except TargetStableError:
             logger.debug('Cannot grant {}'.format(permission))
 
 
diff --git a/devlib/utils/ssh.py b/devlib/utils/ssh.py
index 1342cf2..6b87025 100644
--- a/devlib/utils/ssh.py
+++ b/devlib/utils/ssh.py
@@ -36,7 +36,8 @@ else:
 from pexpect import EOF, TIMEOUT, spawn
 
 # pylint: disable=redefined-builtin,wrong-import-position
-from devlib.exception import HostError, TargetError, TimeoutError
+from devlib.exception import (HostError, TargetStableError, TargetNotRespondingError,
+                              TimeoutError)
 from devlib.utils.misc import which, strip_bash_colors, check_output
 from devlib.utils.misc import (escape_single_quotes, escape_double_quotes,
                                escape_spaces)
@@ -72,7 +73,7 @@ def ssh_get_shell(host, username, password=None, keyfile=None, port=None, timeou
             timeout -= time.time() - start_time
             if timeout <= 0:
                 message = 'Could not connect to {}; is the host name correct?'
-                raise TargetError(message.format(host))
+                raise TargetTransientError(message.format(host))
             time.sleep(5)
 
     conn.setwinsize(500, 200)
@@ -194,7 +195,7 @@ class SshConnection(object):
         return self._scp(source, dest, timeout)
 
     def execute(self, command, timeout=None, check_exit_code=True,
-                as_root=False, strip_colors=True): #pylint: disable=unused-argument
+                as_root=False, strip_colors=True, will_succeed=False): #pylint: disable=unused-argument
         if command == '':
             # Empty command is valid but the __devlib_ec stuff below will
             # produce a syntax error with bash. Treat as a special case.
@@ -210,14 +211,19 @@ class SshConnection(object):
                         exit_code = int(exit_code_text)
                         if exit_code:
                             message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
-                            raise TargetError(message.format(exit_code, command, output))
+                            raise TargetStableError(message.format(exit_code, command, output))
                     except (ValueError, IndexError):
                         logger.warning(
                             'Could not get exit code for "{}",\ngot: "{}"'\
                             .format(command, exit_code_text))
                 return output
         except EOF:
-            raise TargetError('Connection lost.')
+            raise TargetNotRespondingError('Connection lost.')
+        except TargetStableError as e:
+            if will_succeed:
+                raise TargetTransientError(e)
+            else:
+                raise
 
     def background(self, command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, as_root=False):
         try:
@@ -231,7 +237,7 @@ class SshConnection(object):
                 command = _give_password(self.password, command)
             return subprocess.Popen(command, stdout=stdout, stderr=stderr, shell=True)
         except EOF:
-            raise TargetError('Connection lost.')
+            raise TargetNotRespondingError('Connection lost.')
 
     def close(self):
         logger.debug('Logging out {}@{}'.format(self.username, self.host))
@@ -352,7 +358,7 @@ class Gem5Connection(TelnetConnection):
         if host is not None:
             host_system = socket.gethostname()
             if host_system != host:
-                raise TargetError("Gem5Connection can only connect to gem5 "
+                raise TargetStableError("Gem5Connection can only connect to gem5 "
                                   "simulations on your current host {}, which "
                                   "differs from the one given {}!"
                                   .format(host_system, host))
@@ -497,16 +503,23 @@ class Gem5Connection(TelnetConnection):
             logger.debug("Pull complete.")
 
     def execute(self, command, timeout=1000, check_exit_code=True,
-                as_root=False, strip_colors=True):
+                as_root=False, strip_colors=True, will_succeed=False):
         """
         Execute a command on the gem5 platform
         """
         # First check if the connection is set up to interact with gem5
         self._check_ready()
 
-        output = self._gem5_shell(command,
-                                  check_exit_code=check_exit_code,
-                                  as_root=as_root)
+        try:
+            output = self._gem5_shell(command,
+                                      check_exit_code=check_exit_code,
+                                      as_root=as_root)
+        except TargetStableError as e:
+            if will_succeed:
+                raise TargetTransientError(e)
+            else:
+                raise
+
         if strip_colors:
             output = strip_bash_colors(output)
         return output
@@ -576,7 +589,7 @@ class Gem5Connection(TelnetConnection):
         # rather than spewing out pexpect errors.
         if gem5_simulation.poll():
             message = "The gem5 process has crashed with error code {}!\n\tPlease see {} for details."
-            raise TargetError(message.format(gem5_simulation.poll(), gem5_out_dir))
+            raise TargetNotRespondingError(message.format(gem5_simulation.poll(), gem5_out_dir))
         else:
             # Let's re-throw the exception in this case.
             raise err
@@ -604,7 +617,7 @@ class Gem5Connection(TelnetConnection):
         lock_file_name = '{}{}_{}.LOCK'.format(self.lock_directory, host, port)
         if os.path.isfile(lock_file_name):
             # There is already a connection to this gem5 simulation
-            raise TargetError('There is already a connection to the gem5 '
+            raise TargetStableError('There is already a connection to the gem5 '
                               'simulation using port {} on {}!'
                               .format(port, host))
 
@@ -623,7 +636,7 @@ class Gem5Connection(TelnetConnection):
                 self._gem5_EOF_handler(gem5_simulation, gem5_out_dir, err)
         else:
             gem5_simulation.kill()
-            raise TargetError("Failed to connect to the gem5 telnet session.")
+            raise TargetNotRespondingError("Failed to connect to the gem5 telnet session.")
 
         gem5_logger.info("Connected! Waiting for prompt...")
 
@@ -718,7 +731,7 @@ class Gem5Connection(TelnetConnection):
     def _gem5_util(self, command):
         """ Execute a gem5 utility command using the m5 binary on the device """
         if self.m5_path is None:
-            raise TargetError('Path to m5 binary on simulated system  is not set!')
+            raise TargetStableError('Path to m5 binary on simulated system  is not set!')
         self._gem5_shell('{} {}'.format(self.m5_path, command))
 
     def _gem5_shell(self, command, as_root=False, timeout=None, check_exit_code=True, sync=True):  # pylint: disable=R0912
@@ -732,7 +745,7 @@ class Gem5Connection(TelnetConnection):
         fails, warn, but continue with the potentially wrong output.
 
         The exit code is also checked by default, and non-zero exit codes will
-        raise a TargetError.
+        raise a TargetStableError.
         """
         if sync:
             self._sync_gem5_shell()
@@ -788,7 +801,7 @@ class Gem5Connection(TelnetConnection):
                 exit_code = int(exit_code_text.split()[0])
                 if exit_code:
                     message = 'Got exit code {}\nfrom: {}\nOUTPUT: {}'
-                    raise TargetError(message.format(exit_code, command, output))
+                    raise TargetStableError(message.format(exit_code, command, output))
             except (ValueError, IndexError):
                 gem5_logger.warning('Could not get exit code for "{}",\ngot: "{}"'.format(command, exit_code_text))
 
@@ -843,7 +856,7 @@ class Gem5Connection(TelnetConnection):
         Check if the gem5 platform is ready
         """
         if not self.ready:
-            raise TargetError('Gem5 is not ready to interact yet')
+            raise TargetTransientError('Gem5 is not ready to interact yet')
 
     def _wait_for_boot(self):
         pass