Repository: pymeasure/pymeasure

Issue:
Allow property setter to read
Some instruments respond to every command, they even acknowledge setting a value.
Right now, the solution is to set `check_set_errors=True` and to read in `check_errors`, which seems more a hack than a solution.

Some solutions:

1. Previously (in some discussion) I proposed to add a further parameter to `control`, which switches between `write` and `ask` in the setter.
Advantage is, that you can choose it for every property.
Disadvantages are, that we have another parameter and that you have to add it to every single property.
2. `control`'s setter calls `set_property` (name to be determined). This method will just call `write` in the default implementation. An instrument might override this method in order to read afterwards and interpret the read response (which might be an acknowledgment or an error).
Advantages: it is declared only once; no additional parameter for `control`; you can raise an appropriate error "setting property xyz failed" (some instrument stores the last written command in order to be able to raise such an error!).
Disadvantage: You cannot do it for a subset of properties. This should be no problem, as mostly an instrument responds to all setters or to no one.

I like the second solution and would like to implement it. The first solution and current hack are given as alternative solutions.

What are your ideas?

Discussion:

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index b2492292..531ae67c 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,7 @@ New adapter and instrument mechanics
 - Channel class added. Instrument.channels and Instrument.ch_X (X is any channel name) are reserved for channel implementations.
 - All instruments are required to accept a :code:`name` argument.
 - Changed: :code:`read_bytes` of all Adapters by default does not stop reading on a termination character, unless the new argument :code:`break_on_termchar` is set to `True`.
+- The parameters :code:`check_get_errors` and :code:`check_set_errors` enable calling methods of the same name. This enables more systematically dealing with instruments that acknowledge every "set" command.
 
 Deprecated features
 -------------------
diff --git a/docs/dev/adding_instruments/properties.rst b/docs/dev/adding_instruments/properties.rst
index bee3ed8d..6a53314d 100644
--- a/docs/dev/adding_instruments/properties.rst
+++ b/docs/dev/adding_instruments/properties.rst
@@ -373,7 +373,10 @@ The same can be also achieved by the `preprocess_reply` keyword argument to :fun
 Checking the instrument for errors
 **********************************
 If you need to separately ask your instrument about its error state after getting/setting, use the parameters :code:`check_get_errors` and :code:`check_set_errors` of :meth:`~pymeasure.instruments.common_base.CommonBase.control`, respectively.
-If those are enabled, the method :meth:`~pymeasure.instruments.Instrument.check_errors` will be called after device communication has concluded.
+If those are enabled, the methods :meth:`~pymeasure.instruments.Instrument.check_get_errors` and :meth:`~pymeasure.instruments.Instrument.check_set_errors`, respectively, will be called be called after device communication has concluded.
+In the default implementation, for simplicity both methods call :meth:`~pymeasure.instruments.Instrument.check_errors`.
+To read the automatic response of instruments that respond to every set command with an acknowledgment or error, override :meth:`~pymeasure.instruments.Instrument.check_set_errors` as needed.
+
 
 Using multiple values
 *********************
diff --git a/docs/dev/adding_instruments/solutions.rst b/docs/dev/adding_instruments/solutions.rst
index c243aaec..e944c31d 100644
--- a/docs/dev/adding_instruments/solutions.rst
+++ b/docs/dev/adding_instruments/solutions.rst
@@ -16,7 +16,7 @@ General issues
 Communication protocol issues
 *****************************
 
-- The instrument answers every message, even a setting command. You can set the setting's :code:`check_set_errors = True` parameter and redefine :func:`check_errors` to read an answer, see :class:`hcp.TC038D <pymeasure.instruments.hcp.TC038D>`
+- The instrument answers every message, even a setting command. You can set the setting's :code:`check_set_errors = True` parameter and redefine :func:`check_set_errors` to read an answer, see :class:`hcp.TC038D <pymeasure.instruments.hcp.TC038D>`
 - Binary, frame-based communication, see :class:`hcp.TC038D <pymeasure.instruments.hcp.TC038D>`
 - All replies have the same length, see :class:`aja.DCXS <pymeasure.instruments.aja.DCXS>`
 - The device generates garbage messages at startup, cluttering the buffer, see :class:`aja.DCXS <pymeasure.instruments.aja.DCXS>`
diff --git a/pymeasure/instruments/attocube/anc300.py b/pymeasure/instruments/attocube/anc300.py
index 431b9e8b..c21dba06 100644
--- a/pymeasure/instruments/attocube/anc300.py
+++ b/pymeasure/instruments/attocube/anc300.py
@@ -22,17 +22,22 @@
 # THE SOFTWARE.
 #
 
+import logging
 import re
 from math import inf
 from warnings import warn
 
 from pymeasure.adapters import Adapter
-from pymeasure.instruments import Instrument
+from pymeasure.instruments import Instrument, Channel
 from pymeasure.instruments.validators import (joined_validators,
                                               strict_discrete_set,
                                               strict_range)
 
 
+log = logging.getLogger(__name__)
+log.addHandler(logging.NullHandler())
+
+
 def strict_length(value, values):
     if len(value) != values:
         raise ValueError(
@@ -60,7 +65,7 @@ truncated_int_array_strict_length = joined_validators(strict_length,
                                                       truncated_int_array)
 
 
-class Axis:
+class Axis(Channel):
     """ Represents a single open loop axis of the Attocube ANC350
 
     :param axis: axis identifier, integer from 1 to 7
@@ -68,38 +73,35 @@ class Axis:
     """
 
     serial_nr = Instrument.measurement("getser",
-                                       "Serial number of the axis")
+                                       "Get the serial number of the axis.")
 
     voltage = Instrument.control(
         "getv", "setv %.3f",
-        """ Amplitude of the stepping voltage in volts from 0 to 150 V. This
-        property can be set. """,
+        """Control the amplitude of the stepping voltage in volts from 0 to 150 V.""",
         validator=strict_range, values=[0, 150], check_set_errors=True)
 
     frequency = Instrument.control(
         "getf", "setf %.3f",
-        """ Frequency of the stepping motion in Hertz from 1 to 10000 Hz.
-        This property can be set. """,
+        """Control the frequency of the stepping motion in Hertz from 1 to 10000 Hz.""",
         validator=strict_range, values=[1, 10000],
         cast=int, check_set_errors=True)
 
     mode = Instrument.control(
         "getm", "setm %s",
-        """ Axis mode. This can be 'gnd', 'inp', 'cap', 'stp', 'off',
-        'stp+', 'stp-'. Available modes depend on the actual axis model""",
+        """Control axis mode. This can be 'gnd', 'inp', 'cap', 'stp', 'off',
+        'stp+', 'stp-'. Available modes depend on the actual axis model.""",
         validator=strict_discrete_set,
         values=['gnd', 'inp', 'cap', 'stp', 'off', 'stp+', 'stp-'],
         check_set_errors=True)
 
     offset_voltage = Instrument.control(
         "geta", "seta %.3f",
-        """ Offset voltage in Volts from 0 to 150 V.
-        This property can be set. """,
+        """Control offset voltage in Volts from 0 to 150 V.""",
         validator=strict_range, values=[0, 150], check_set_errors=True)
 
     pattern_up = Instrument.control(
         "getpu", "setpu %s",
-        """ step up pattern of the piezo drive. 256 values ranging from 0
+        """Control step up pattern of the piezo drive. 256 values ranging from 0
         to 255 representing the the sequence of output voltages within one
         step of the piezo drive. This property can be set, the set value
         needs to be an array with 256 integer values. """,
@@ -110,7 +112,7 @@ class Axis:
 
     pattern_down = Instrument.control(
         "getpd", "setpd %s",
-        """ step down pattern of the piezo drive. 256 values ranging from 0
+        """Control step down pattern of the piezo drive. 256 values ranging from 0
         to 255 representing the the sequence of output voltages within one
         step of the piezo drive. This property can be set, the set value
         needs to be an array with 256 integer values. """,
@@ -121,52 +123,38 @@ class Axis:
 
     output_voltage = Instrument.measurement(
         "geto",
-        """ Output voltage in volts.""")
+        """Measure the output voltage in volts.""")
 
     capacity = Instrument.measurement(
         "getc",
-        """ Saved capacity value in nF of the axis.""")
+        """Measure the saved capacity value in nF of the axis.""")
 
     stepu = Instrument.setting(
         "stepu %d",
-        """ Step upwards for N steps. Mode must be 'stp' and N must be
+        """Set the steps upwards for N steps. Mode must be 'stp' and N must be
         positive.""",
         validator=strict_range, values=[0, inf], check_set_errors=True)
 
     stepd = Instrument.setting(
         "stepd %d",
-        """ Step downwards for N steps. Mode must be 'stp' and N must be
+        """Set the steps downwards for N steps. Mode must be 'stp' and N must be
         positive.""",
         validator=strict_range, values=[0, inf], check_set_errors=True)
 
-    def __init__(self, controller, axis):
-        self.axis = str(axis)
-        self.controller = controller
-
-    def _add_axis_id(self, command):
-        """ add axis id to a command string at the correct position after the
-        initial command, but before a potential value
+    def insert_id(self, command):
+        """Insert the channel id in a command replacing `placeholder`.
 
-        :param str command: command string
-        :returns: command string with added axis id
+        Add axis id to a command string at the correct position after the
+        initial command, but before a potential value.
         """
         cmdparts = command.split()
-        cmdparts.insert(1, self.axis)
+        cmdparts.insert(1, self.id)
         return ' '.join(cmdparts)
 
-    def ask(self, command, **kwargs):
-        return self.controller.ask(self._add_axis_id(command), **kwargs)
-
-    def write(self, command, **kwargs):
-        return self.controller.write(self._add_axis_id(command), **kwargs)
-
-    def values(self, command, **kwargs):
-        return self.controller.values(self._add_axis_id(command), **kwargs)
-
     def stop(self):
         """ Stop any motion of the axis """
         self.write('stop')
-        self.check_errors()
+        self.check_set_errors()
 
     def move(self, steps, gnd=True):
         """ Move 'steps' steps in the direction given by the sign of the
@@ -189,7 +177,7 @@ class Axis:
         else:
             pass  # do not set stepu/d to 0 since it triggers a continous move
         # wait for the move to finish
-        self.controller.wait_for(abs(steps) / self.frequency)
+        self.parent.wait_for(abs(steps) / self.frequency)
         # ask if movement finished
         self.ask('stepw')
         if gnd:
@@ -203,15 +191,11 @@ class Axis:
         """
         self.mode = 'cap'
         # wait for the measurement to finish
-        self.controller.wait_for(1)
+        self.parent.wait_for(1)
         # ask if really finished
         self.ask('capw')
         return self.capacity
 
-    def check_errors(self):
-        """Read after setting a setting or control."""
-        self.controller.check_errors()
-
 
 class ANC300Controller(Instrument):
     """ Attocube ANC300 Piezo stage controller with several axes
@@ -274,7 +258,7 @@ class ANC300Controller(Instrument):
 
         self._axisnames = axisnames
         for i, axis in enumerate(axisnames):
-            setattr(self, axis, Axis(self, i + 1))
+            setattr(self, axis, self.add_child(Axis, id=str(i + 1)))
 
         self.wait_for()
         # clear messages sent upon opening the connection,
@@ -290,9 +274,20 @@ class ANC300Controller(Instrument):
         # switch console echo off
         self.ask('echo off')
 
-    def check_errors(self):
-        """Read after setting a value."""
-        self.read()
+    def check_set_errors(self):
+        """Check for errors after having set a property and log them.
+
+        Called if :code:`check_set_errors=True` is set for that property.
+
+        :return: List of error entries.
+        """
+        try:
+            self.read()
+        except Exception as exc:
+            log.exception("Setting a property failed.", exc_info=exc)
+            raise
+        else:
+            return []
 
     def ground_all(self):
         """ Grounds all axis of the controller. """
diff --git a/pymeasure/instruments/channel.py b/pymeasure/instruments/channel.py
index 858c46f6..d88deb49 100644
--- a/pymeasure/instruments/channel.py
+++ b/pymeasure/instruments/channel.py
@@ -103,12 +103,34 @@ class Channel(CommonBase):
         return self.parent.read_binary_values(**kwargs)
 
     def check_errors(self):
-        """Read all errors from the instrument.
+        """Read all errors from the instrument and log them.
 
-        :return: list of error entries
+        :return: List of error entries.
         """
         return self.parent.check_errors()
 
+    def check_get_errors(self):
+        """Check for errors after having gotten a property and log them.
+
+        Called if :code:`check_get_errors=True` is set for that property.
+
+        If you override this method, you may choose to raise an Exception for certain errors.
+
+        :return: List of error entries.
+        """
+        return self.parent.check_get_errors()
+
+    def check_set_errors(self):
+        """Check for errors after having set a property and log them.
+
+        Called if :code:`check_set_errors=True` is set for that property.
+
+        If you override this method, you may choose to raise an Exception for certain errors.
+
+        :return: List of error entries.
+        """
+        return self.parent.check_set_errors()
+
     # Communication functions
     def wait_for(self, query_delay=0):
         """Wait for some time. Used by 'ask' to wait before reading.
diff --git a/pymeasure/instruments/common_base.py b/pymeasure/instruments/common_base.py
index 1edccb78..8f623f22 100644
--- a/pymeasure/instruments/common_base.py
+++ b/pymeasure/instruments/common_base.py
@@ -476,7 +476,16 @@ class CommonBase:
                                maxsplit=maxsplit,
                                **values_kwargs)
             if check_get_errors:
-                self.check_errors()
+                try:
+                    error_list = self.check_get_errors()
+                except Exception as exc:
+                    log.error("Exception raised while getting a property with the command "
+                              f"""'{command_process(get_command)}': '{str(exc)}'.""")
+                    raise
+                errors = [str(error) for error in error_list]
+                if errors:
+                    log.error("Error received after trying to get a property with the command "
+                              f"""'{command_process(get_command)}': '{"', '".join(errors)}'.""")
             if len(vals) == 1:
                 value = get_process(vals[0])
                 if not map_values:
@@ -525,7 +534,18 @@ class CommonBase:
                 )
             self.write(command_process(set_command) % value)
             if check_set_errors:
-                self.check_errors()
+                try:
+                    error_list = self.check_set_errors()
+                except Exception as exc:
+                    log.error("Exception raised while setting a property with the command "
+                              f"""'{command_process(set_command) % value}': '{str(exc)}'.""")
+                    raise
+                errors = [str(error) for error in error_list]
+                if errors:
+                    log.error(
+                        "Error received after trying to set a property with the command "
+                        f"""'{command_process(set_command) % value}': '{"', '".join(errors)}'."""
+                    )
 
         # Add the specified document string to the getter
         fget.__doc__ = docs
@@ -644,3 +664,32 @@ class CommonBase:
                                   check_set_errors=check_set_errors,
                                   dynamic=dynamic,
                                   )
+
+    def check_errors(self):
+        """Read all errors from the instrument and log them.
+
+        :return: List of error entries.
+        """
+        raise NotImplementedError("Implement it in a subclass.")
+
+    def check_get_errors(self):
+        """Check for errors after having gotten a property and log them.
+
+        Called if :code:`check_get_errors=True` is set for that property.
+
+        If you override this method, you may choose to raise an Exception for certain errors.
+
+        :return: List of error entries.
+        """
+        raise NotImplementedError("Implement it in a subclass.")
+
+    def check_set_errors(self):
+        """Check for errors after having set a property and log them.
+
+        Called if :code:`check_set_errors=True` is set for that property.
+
+        If you override this method, you may choose to raise an Exception for certain errors.
+
+        :return: List of error entries.
+        """
+        raise NotImplementedError("Implement it in a subclass.")
diff --git a/pymeasure/instruments/hcp/tc038.py b/pymeasure/instruments/hcp/tc038.py
index 9573d372..2fb206e3 100644
--- a/pymeasure/instruments/hcp/tc038.py
+++ b/pymeasure/instruments/hcp/tc038.py
@@ -22,10 +22,16 @@
 # THE SOFTWARE.
 #
 
+import logging
+
 from pymeasure.instruments import Instrument
 from pyvisa.constants import Parity
 
 
+log = logging.getLogger(__name__)
+log.addHandler(logging.NullHandler())
+
+
 def _data_to_temp(data):
     """Convert the returned hex value "data" to a temperature in °C."""
     return int(data[7:11], 16) / 10.
@@ -100,9 +106,18 @@ class TC038(Instrument):
             raise ConnectionError(errors[0])
         return got
 
-    def check_errors(self):
-        """Read the error from the instrument and return a list of errors."""
-        self.read()
+    def check_set_errors(self):
+        """Check for errors after having set a property.
+
+        Called if :code:`check_set_errors=True` is set for that property.
+        """
+        try:
+            self.read()
+        except ConnectionError as exc:
+            log.exception("Setting a property failed.", exc_info=exc)
+            raise
+        else:
+            return []
 
     def set_monitored_quantity(self, quantity='temperature'):
         """
@@ -123,23 +138,23 @@ class TC038(Instrument):
         get_process=_data_to_temp,
         set_process=lambda temp: f"{int(round(temp * 10)):04X}",
         check_set_errors=True,
-        )
+    )
 
     temperature = Instrument.measurement(
         "WRD" + registers['temperature'] + ",01",
         """Measure the current temperature in °C.""",
         get_process=_data_to_temp
-        )
+    )
 
     monitored_value = Instrument.measurement(
         "WRM",
         """Measure the currently monitored value. For default it is the current
         temperature in °C.""",
         get_process=_data_to_temp
-        )
+    )
 
     information = Instrument.measurement(
         "INF6",
         """Get the information about the device and its capabilites.""",
         get_process=lambda got: got[7:-1],
-        )
+    )
diff --git a/pymeasure/instruments/hcp/tc038d.py b/pymeasure/instruments/hcp/tc038d.py
index f57b299b..77f2fae5 100644
--- a/pymeasure/instruments/hcp/tc038d.py
+++ b/pymeasure/instruments/hcp/tc038d.py
@@ -22,11 +22,17 @@
 # THE SOFTWARE.
 #
 
+import logging
+
 from enum import IntEnum
 
 from pymeasure.instruments import Instrument
 
 
+log = logging.getLogger(__name__)
+log.addHandler(logging.NullHandler())
+
+
 def CRC16(data):
     """Calculate the CRC16 checksum for the data byte array."""
     CRC = 0xFFFF
@@ -132,9 +138,18 @@ class TC038D(Instrument):
             else:
                 raise ConnectionError(f"Unknown read error. Received: {got} {end}")
 
-    def check_errors(self):
-        """To be called from the property setters to read the acknowledgment."""
-        self.read()
+    def check_set_errors(self):
+        """Check for errors after having set a property.
+
+        Called if :code:`check_set_errors=True` is set for that property.
+        """
+        try:
+            self.read()
+        except Exception as exc:
+            log.exception("Setting a property failed.", exc_info=exc)
+            raise
+        else:
+            return []
 
     def ping(self, test_data=0):
         """Test the connection sending an integer up to 65535, checks the response."""
diff --git a/pymeasure/instruments/instrument.py b/pymeasure/instruments/instrument.py
index 62f65780..4c3ec07a 100644
--- a/pymeasure/instruments/instrument.py
+++ b/pymeasure/instruments/instrument.py
@@ -205,9 +205,9 @@ class Instrument(CommonBase):
         log.info(f"Finished shutting down {self.name}")
 
     def check_errors(self):
-        """ Read all errors from the instrument.
+        """Read all errors from the instrument and log them.
 
-        :return: list of error entries
+        :return: List of error entries.
         """
         if self.SCPI:
             errors = []
@@ -221,3 +221,25 @@ class Instrument(CommonBase):
             return errors
         else:
             raise NotImplementedError("Non SCPI instruments require implementation in subclasses")
+
+    def check_get_errors(self):
+        """Check for errors after having gotten a property and log them.
+
+        Called if :code:`check_get_errors=True` is set for that property.
+
+        If you override this method, you may choose to raise an Exception for certain errors.
+
+        :return: List of error entries.
+        """
+        return self.check_errors()
+
+    def check_set_errors(self):
+        """Check for errors after having set a property and log them.
+
+        Called if :code:`check_set_errors=True` is set for that property.
+
+        If you override this method, you may choose to raise an Exception for certain errors.
+
+        :return: List of error entries.
+        """
+        return self.check_errors()
diff --git a/pymeasure/instruments/ipgphotonics/yar.py b/pymeasure/instruments/ipgphotonics/yar.py
index 442d066a..78055ce2 100644
--- a/pymeasure/instruments/ipgphotonics/yar.py
+++ b/pymeasure/instruments/ipgphotonics/yar.py
@@ -22,12 +22,17 @@
 # THE SOFTWARE.
 #
 
+import logging
 from enum import IntFlag
 
 from pymeasure.instruments import Instrument, validators
 from pyvisa.constants import Parity, StopBits
 
 
+log = logging.getLogger(__name__)
+log.addHandler(logging.NullHandler())
+
+
 def emission_validator(value, values):
     if value is True:
         return "ON"
@@ -94,15 +99,22 @@ class YAR(Instrument):
         """Read an instrument answer and check whether it is an error."""
         reply = super().read().split(":")
         if reply[0] == "ERR":
-            raise ConnectionError(f"Reading error {reply}")
+            raise ConnectionError(f"Reading error '{reply}'.")
         else:
             return reply[-1].strip()
 
-    def check_errors(self):
-        """Empty the read buffer."""
-        got = self.read().split(":")
-        if got[0] == "ERR":
-            raise ConnectionError(f"Connection error {got[-1]}.")
+    def check_set_errors(self):
+        """Check for errors after having set a property.
+
+        Called if :code:`check_set_errors=True` is set for that property.
+        """
+        try:
+            self.read()
+        except ConnectionError as exc:
+            log.exception("Setting a property failed.", exc_info=exc)
+            raise
+        else:
+            return []
 
     # COMMUNICATION FUNCTIONS
 
diff --git a/pymeasure/instruments/mksinst/mks937b.py b/pymeasure/instruments/mksinst/mks937b.py
index e7c797f9..ab2265ab 100644
--- a/pymeasure/instruments/mksinst/mks937b.py
+++ b/pymeasure/instruments/mksinst/mks937b.py
@@ -141,17 +141,17 @@ class MKS937B(Instrument):
 
     def write(self, command):
         """
-        Writes to the instrument including the device address
+        Write to the instrument including the device address.
 
         :param command: command string to be sent to the instrument
         """
         super().write(self._prepend_address(command))
 
-    def check_errors(self):
+    def check_set_errors(self):
         """
-        check reply string for acknowledgement string
+        Check reply string for acknowledgement string.
         """
-        ret = self.adapter.read()  # use adapter read to get raw reply
+        ret = super().read()  # use super read to get raw reply
         reply = self._re_response.search(ret)
         if reply:
             if reply.group('ack') == 'ACK':
diff --git a/pymeasure/instruments/toptica/ibeamsmart.py b/pymeasure/instruments/toptica/ibeamsmart.py
index 65b1365d..e7ca74ae 100644
--- a/pymeasure/instruments/toptica/ibeamsmart.py
+++ b/pymeasure/instruments/toptica/ibeamsmart.py
@@ -144,8 +144,7 @@ class IBeamSmart(Instrument):
         reply = super().read()  # read back the LF+CR which is always sent back
         if reply != "":
             raise ValueError(
-                "TopticaAdapter.read(1): Error after command "
-                f"'{self.lastcommand}' with message '{reply}'")
+                f"Error, no empty line at begin of message, instead '{reply}'")
         msg = []
         try:
             while True:
@@ -159,9 +158,7 @@ class IBeamSmart(Instrument):
                 self.adapter.connection.flush_read_buffer()
             except AttributeError:
                 log.warning("Adapter does not have 'flush_read_buffer' method.")
-            raise ValueError(
-                "TopticaAdapter.read(2): Error after command "
-                f"'{self.lastcommand}' with message '{reply}'")
+            raise ValueError(f"Flush buffer failed after '{reply}'")
         reply = '\n'.join(msg)
         r = self._reg_value.search(reply)
         if r:
@@ -169,12 +166,8 @@ class IBeamSmart(Instrument):
         else:
             return reply
 
-    def write(self, command):
-        self.lastcommand = command
-        super().write(command)
-
-    def check_errors(self):
-        """Check communication after setting a value.
+    def check_set_errors(self):
+        """Check for errors after having gotten a property and log them.
 
         Checks if the last reply is only '[OK]', otherwise a ValueError is
         raised and the read buffer is flushed because one has to assume that
@@ -184,9 +177,9 @@ class IBeamSmart(Instrument):
         if reply:
             # anything else than '[OK]'.
             self.adapter.connection.flush_read_buffer()
-            raise ValueError(
-                f"IBeamSmart: Error after command '{self.lastcommand}' with "
-                f"message '{reply}'")
+            log.error(f"Setting a property failed with reply '{reply}'.")
+            raise ValueError(f"Setting a property failed with reply '{reply}'.")
+        return []
 
     version = Instrument.measurement(
         "ver", """Get Firmware version number.""",
@@ -276,7 +269,7 @@ class IBeamSmart(Instrument):
     def enable_continous(self):
         """Enable countinous emmission mode."""
         self.write('di ext')
-        self.check_errors()
+        self.check_set_errors()
         self.emission = True
         self.ch_2.enabled = True
 
@@ -288,12 +281,12 @@ class IBeamSmart(Instrument):
         self.emission = True
         self.ch_2.enabled = True
         self.write('en ext')
-        self.check_errors()
+        self.check_set_errors()
 
     def disable(self):
         """Shutdown all laser operation."""
         self.write('di 0')
-        self.check_errors()
+        self.check_set_errors()
         self.emission = False
 
     def shutdown(self):
diff --git a/pymeasure/instruments/velleman/velleman_k8090.py b/pymeasure/instruments/velleman/velleman_k8090.py
index 8211aa92..366081ae 100644
--- a/pymeasure/instruments/velleman/velleman_k8090.py
+++ b/pymeasure/instruments/velleman/velleman_k8090.py
@@ -23,12 +23,17 @@
 #
 
 from enum import IntFlag
+import logging
 
 from pyvisa import VisaIOError
 from pymeasure.adapters import SerialAdapter, VISAAdapter
 from pymeasure.instruments import Instrument
 
 
+log = logging.getLogger(__name__)
+log.addHandler(logging.NullHandler())
+
+
 class VellemanK8090Switches(IntFlag):
     """Use to identify switch channels."""
 
@@ -254,15 +259,24 @@ class VellemanK8090(Instrument):
 
         return ",".join(values_str)
 
-    def check_errors(self):
-        """Called after each set command, normally to look for errors.
+    def check_set_errors(self):
+        """Check for errors after having set a property and log them.
+
+        Called if :code:`check_set_errors=True` is set for that property.
 
         The K8090 replies with a status after a switch command, but
         **only** after any switch actually changed. In order to guarantee
         the buffer is empty, we attempt to read it fully here.
         No actual error checking is done here!
+
+        :return: List of error entries.
         """
         try:
             self.read()
         except (VisaIOError, ConnectionError):
             pass  # Ignore a timeout
+        except Exception as exc:
+            log.exception("Setting a property failed.", exc_info=exc)
+            raise
+        else:
+            return []


Test patch:
diff --git a/pymeasure/instruments/eurotest/eurotestHPP120256.py b/pymeasure/instruments/eurotest/eurotestHPP120256.py
index cf801e43..a0583105 100644
--- a/pymeasure/instruments/eurotest/eurotestHPP120256.py
+++ b/pymeasure/instruments/eurotest/eurotestHPP120256.py
@@ -44,46 +44,45 @@ class EurotestHPP120256(Instrument):
 
     .. code-block:: python
 
-    hpp120256 = EurotestHPP120256("GPIB0::20::INSTR")
+        hpp120256 = EurotestHPP120256("GPIB0::20::INSTR")
 
-    print(hpp120256.id)
-    print(hpp120256.lam_status)
-    print(hpp120256.status)
+        print(hpp120256.id)
+        print(hpp120256.lam_status)
+        print(hpp120256.status)
 
-    hpp120256.ramp_to_zero(100.0)
+        hpp120256.ramp_to_zero(100.0)
 
-    hpp120256.voltage_ramp = 50.0  # V/s
-    hpp120256.current_limit = 2.0  # mA
-    inst.kill_enabled = True  # Enable over-current protection
-    time.sleep(1.0)  # Give time to enable kill
-    inst.output_enabled = True
-    time.sleep(1.0)  # Give time to output on
+        hpp120256.voltage_ramp = 50.0  # V/s
+        hpp120256.current_limit = 2.0  # mA
+        inst.kill_enabled = True  # Enable over-current protection
+        time.sleep(1.0)  # Give time to enable kill
+        inst.output_enabled = True
+        time.sleep(1.0)  # Give time to output on
 
-    abs_output_voltage_error = 0.02 # kV
+        abs_output_voltage_error = 0.02 # kV
 
-    hpp120256.wait_for_output_voltage_reached(abs_output_voltage_error, 1.0, 40.0)
+        hpp120256.wait_for_output_voltage_reached(abs_output_voltage_error, 1.0, 40.0)
 
-    # Here voltage HV output should be at 0.0 kV
+        # Here voltage HV output should be at 0.0 kV
 
-    print("Setting the output voltage to 1.0kV...")
-    hpp120256.voltage_setpoint = 1.0  # kV
+        print("Setting the output voltage to 1.0kV...")
+        hpp120256.voltage_setpoint = 1.0  # kV
 
-    # Now HV output should be rising to reach the 1.0kV at 50.0 V/s
+        # Now HV output should be rising to reach the 1.0kV at 50.0 V/s
 
-    hpp120256.wait_for_output_voltage_reached(abs_output_voltage_error, 1.0, 40.0)
+        hpp120256.wait_for_output_voltage_reached(abs_output_voltage_error, 1.0, 40.0)
 
-    # Here voltage HV output should be at 1.0 kV
+        # Here voltage HV output should be at 1.0 kV
 
-    hpp120256.shutdown()
+        hpp120256.shutdown()
 
-    hpp120256.wait_for_output_voltage_reached(abs_output_voltage_error, 1.0, 60.0)
+        hpp120256.wait_for_output_voltage_reached(abs_output_voltage_error, 1.0, 60.0)
 
-    # Here voltage HV output should be at 0.0 kV
+        # Here voltage HV output should be at 0.0 kV
 
-    inst.output_enabled = False
-
-    # Now the HV voltage source is in safe state
+        inst.output_enabled = False
 
+        # Now the HV voltage source is in safe state
     """
 
     VOLTAGE_RANGE = [0.0, 12.0]  # kVolts
diff --git a/tests/instruments/attocube/test_anc300.py b/tests/instruments/attocube/test_anc300.py
index 17d72eec..ccad654b 100644
--- a/tests/instruments/attocube/test_anc300.py
+++ b/tests/instruments/attocube/test_anc300.py
@@ -34,7 +34,7 @@ init_comm = [
     (None, "Authorization success"),
     ("echo off", "> echo off"),
     (None, "OK"),
-    ]
+]
 
 
 def test_stepu():
diff --git a/tests/instruments/test_all_instruments.py b/tests/instruments/test_all_instruments.py
index c44e13bb..8a6b043c 100644
--- a/tests/instruments/test_all_instruments.py
+++ b/tests/instruments/test_all_instruments.py
@@ -124,7 +124,6 @@ grandfathered_docstring_instruments = [
     "Agilent33521A",
     "Agilent34410A",
     "Agilent34450A",
-    "Agilent34450A",
     "Agilent4156",
     "SMU",  # agilent/agilent4156
     "VMU",  # agilent/agilent4156
@@ -135,13 +134,10 @@ grandfathered_docstring_instruments = [
     "VARD",  # agilent/agilent4156
     "Agilent8257D",
     "Agilent8722ES",
-    "Agilent8722ES",
     "AgilentB1500",
     "AgilentE4408B",
     "AgilentE4980",
     "Ametek7270",
-    "Ametek7270",
-    "AMI430",
     "AMI430",
     "DPSeriesMotorController",
     "APSIN12G",
@@ -161,10 +157,8 @@ grandfathered_docstring_instruments = [
     "HP6633A",
     "HP6634A",
     "HP8116A",
-    "HP8116A",
     "Keithley2000",
     "Keithley2260B",
-    "Keithley2260B",
     "Keithley2306",
     "Keithley2400",
     "Keithley2450",
@@ -176,11 +170,8 @@ grandfathered_docstring_instruments = [
     "KeysightDSOX1102G",
     "KeysightN5767A",
     "KeysightN7776C",
-    "KeysightN7776C",
-    "KeysightN7776C",
     "LakeShore421",
     "LakeShore425",
-    "LakeShore425",
     "LakeShoreTemperatureChannel",
     "LakeShoreHeaterChannel",
     "LeCroyT3DSO1204",
@@ -189,9 +180,6 @@ grandfathered_docstring_instruments = [
     "ITC503",
     "PS120_10",
     "ParkerGV6",
-    "ParkerGV6",
-    "ParkerGV6",
-    "ParkerGV6",
     "CNT91",
     "razorbillRP100",
     "FSL",
@@ -202,23 +190,16 @@ grandfathered_docstring_instruments = [
     "SPDSingleChannelBase",
     "SPDBase",
     "DSP7265",
-    "DSP7265",
-    "DSP7265",
-    "DSP7265",
-    "DSP7265",
     "SG380",
     "SR510",
     "SR570",
     "SR830",
-    "SR830",
-    "SR830",
     "SR860",
     "ATS525",
     "ATS545",
     "ATSBase",
     "ECO560",
     "TexioPSW360L30",
-    "TexioPSW360L30",
     "Thermotron3800",
     "VellemanK8090",
     "Yokogawa7651",
diff --git a/tests/instruments/test_channel.py b/tests/instruments/test_channel.py
index dba037b4..16e214db 100644
--- a/tests/instruments/test_channel.py
+++ b/tests/instruments/test_channel.py
@@ -116,6 +116,18 @@ class TestChannelCommunication:
         ch.read_binary_values()
         assert ch.parent.method_calls == [mock.call.read_binary_values()]
 
+    def test_check_errors(self, ch):
+        ch.check_errors()
+        assert ch.parent.method_calls == [mock.call.check_errors()]
+
+    def test_check_get_errors(self, ch):
+        ch.check_get_errors()
+        assert ch.parent.method_calls == [mock.call.check_get_errors()]
+
+    def test_check_set_errors(self, ch):
+        ch.check_set_errors()
+        assert ch.parent.method_calls == [mock.call.check_set_errors()]
+
 
 def test_channel_with_different_prefix():
     c = ChannelWithPlaceholder(None, "A")
diff --git a/tests/instruments/test_common_base.py b/tests/instruments/test_common_base.py
index b5008f0f..cc05e142 100644
--- a/tests/instruments/test_common_base.py
+++ b/tests/instruments/test_common_base.py
@@ -22,6 +22,8 @@
 # THE SOFTWARE.
 #
 
+import logging
+
 import pytest
 
 from pymeasure.units import ureg
@@ -108,6 +110,14 @@ class FakeBase(CommonBaseTesting):
         map_values=True,
         dynamic=True,
     )
+    fake_ctrl_errors = CommonBase.control(
+        "ge", "se %d", "Fake control for getting errors after getting/setting values.",
+        validator=truncated_range,
+        values=(1, 10),
+        dynamic=True,
+        check_get_errors=True,
+        check_set_errors=True
+    )
 
 
 @pytest.fixture()
@@ -371,24 +381,80 @@ def test_control_doc(dynamic):
     assert Fake.x.__doc__ == expected_doc
 
 
-def test_control_check_errors_get(fake):
-    fake.fake_ctrl_check_get_errors = True
+def test_check_errors_not_implemented(fake):
+    with pytest.raises(NotImplementedError):
+        fake.check_errors()
+
 
+def test_check_get_errors_not_implemented(fake):
+    with pytest.raises(NotImplementedError):
+        fake.check_get_errors()
+
+
+def test_control_check_get_errors(fake, caplog):
     def checking():
         fake.error = True
-    fake.check_errors = checking
-    fake.fake_ctrl
+        return [(7, "some error")]
+    fake.check_get_errors = checking
+    fake.fake_ctrl_errors
     assert fake.error is True
+    assert caplog.record_tuples[-1] == (
+        "pymeasure.instruments.common_base",
+        logging.ERROR,
+        "Error received after trying to get a property with the command 'ge': '(7, 'some error')'."
+    )
+
+
+def test_control_check_get_errors_multiple_errors(fake, caplog):
+    def checking():
+        fake.error = True
+        return [15, (19, "x")]
+    fake.check_get_errors = checking
+    fake.fake_ctrl_errors
+    assert caplog.record_tuples[-1] == (
+        "pymeasure.instruments.common_base",
+        logging.ERROR,
+        "Error received after trying to get a property with the command 'ge': '15', '(19, 'x')'."
+    )
+
+
+def test_control_check_get_errors_log_exception(fake, caplog):
+    with pytest.raises(NotImplementedError):
+        fake.fake_ctrl_errors
+    assert caplog.record_tuples[-1] == (
+        "pymeasure.instruments.common_base",
+        logging.ERROR,
+        "Exception raised while getting a property with the command 'ge': 'Implement it in a subclass.'."  # noqa: E501
+    )
 
 
-def test_control_check_errors_set(fake):
-    fake.fake_ctrl_check_set_errors = True
+def test_check_set_errors_not_implemented(fake):
+    with pytest.raises(NotImplementedError):
+        fake.check_set_errors()
 
+
+def test_control_check_set_errors(fake, caplog):
     def checking():
         fake.error = True
-    fake.check_errors = checking
-    fake.fake_ctrl = 7
+        return [(7, "Error!")]
+    fake.check_set_errors = checking
+    fake.fake_ctrl_errors = 7
     assert fake.error is True
+    assert caplog.record_tuples[-1] == (
+        "pymeasure.instruments.common_base",
+        logging.ERROR,
+        "Error received after trying to set a property with the command 'se 7': '(7, 'Error!')'."
+    )
+
+
+def test_control_check_set_errors_log_exception(fake, caplog):
+    with pytest.raises(NotImplementedError):
+        fake.fake_ctrl_errors = 7
+    assert caplog.record_tuples[-1] == (
+        "pymeasure.instruments.common_base",
+        logging.ERROR,
+        "Exception raised while setting a property with the command 'se 7': 'Implement it in a subclass.'."  # noqa: E501
+    )
 
 
 @pytest.mark.parametrize("dynamic", [False, True])