UniteLabs
How-to

Error Handling

Errors happen. This guide shows how to handle them gracefully in your SiLA 2 connector - so clients and users stay informed, not confused.

Error handling is a crucial aspect of developing reliable connectors for laboratory instruments. A connector must anticipate failures from various sources - whether due to invalid user input, communication failures, or hardware malfunctions - and handle them appropriately. This guideline explains how to manage these errors, document them effectively, and expose them via the SiLA 2 interface so that both client applications and users of the interface can understand and respond appropriately.

Errors encountered in connector development typically fall into one of four categories:

  • Communication Errors: Failures in the underlying infrastructure.
  • Framework Errors: Violations of the SiLA 2 protocol or client misuse.
  • Validation Errors: Input that violates constraints, either defined at design-time or evaluated dynamically at runtime.
  • Execution Errors: Unexpected failures during the actual operation of a command or property.

Each of these categories requires a different strategy for detection, reporting, and documentation.

Communication Errors

Communication failures occur at the system or transport layer and often originate from sources beyond your direct control, such as the operating system, network stack, or transport protocol. These failures may include issues like a loss of network connectivity or receiving corrupted or invalid protobuf messages. While they tend to be transient, they must still be reported to the client in a clear and user-friendly manner.

Typically, communication failures are caught and handled by the underlying SiLA framework. However, you should still ensure your connector can recover from such interruptions gracefully and maintain a consistent internal state. For example, if the transport layer fails mid-command, the framework may abort execution, and your connector must avoid side effects from partial execution.

Framework Errors

Framework-level errors result from violations of the SiLA 2 protocol or incorrect client behavior. These may include scenarios where a client sends disallowed metadata or tries to execute a command after the server has explicitly disallowed further execution. In such cases, the SiLA framework automatically detects the violation and raises an appropriate error, requiring little or no intervention from connector developers.

While you do not typically need to handle these errors yourself, understanding their origin can be helpful for diagnosing client-side integration issues. When debugging such problems, check whether the SiLA framework has rejected the request due to a protocol constraint or misuse of the interface.

One example is CommandExecutionNotAccepted, which the framework raises automatically when a client calls a command configured with sila.ExecutionMode.SINGLE while a previous invocation is still running - see the Execution Modes guide for details.

Validation Errors

Connectors often need to enforce constraints on command input parameters. These constraints can be enforced at three levels:

  1. Design-time, using the SiLA framework's built-in constraint system.
  2. Startup, setting SiLA constraints based on device configuration.
  3. Runtime, checking the parameter values in the connector implementation.

The first two are part of the feature definition, so a client knows the valid range before it sends anything. If they are implemented using SiLA constraints as described in the constraints section of the data types guide, a ValidationError with details about the violation is raised automatically on invalid inputs. These errors are informative and predictable, making them ideal for user guidance.

Validating in the Implementation

Some rules cannot be expressed as a constraint. A constraint applies to a single parameter, and its valid range is fixed once the server has started - so a value that is only invalid in relation to another parameter, such as a target that must stay below a maximum supplied in the same call, has to be checked by the connector itself.

Register a validation for a command using that command's validator decorator, and raise a ValidationError naming the parameter at fault:

from unitelabs.cdk import sila


class TemperatureController(sila.Feature):
    @sila.ObservableCommand()
    async def ramp_temperature(self, target: float, maximum: float) -> None:
        """
        Ramp the sample chamber to the target temperature.

        Args:
          Target: The temperature to ramp to, in degrees Celsius.
          Maximum: The upper bound for this run, in degrees Celsius.
        """

        await self.io.ramp(target)

    @ramp_temperature.validator
    def _check_target(self, target: float, maximum: float) -> None:
        if target > maximum:
            msg = f"Target must not exceed the maximum of {maximum} degrees Celsius."
            raise sila.errors.ValidationError(msg, "target")

Listing 1: Rejecting parameter values that are only invalid in relation to one another.

The validation runs before the command implementation, and the command only executes if it returns without raising. Name the offending parameter by its Python parameter name - "target" above, not the Target display name from the docstring - and the fully qualified parameter identifier the client expects is filled in for you.

A validation only has to declare the parameters it actually inspects, so the signature for a check on one parameter of a command with many stays short. It may be a regular or an async method.

A command may register more than one validation, which keeps independent checks separate. They run in the order in which they were registered, and the first one to reject the parameters stops the command - the ones after it do not run.

Every parameter check belongs in a validation: raise a ValidationError there and never from the body of a command. Once a command runs, its parameters count as accepted - an observable command execution has already been confirmed to the client by then, and the SiLA specification does not allow its result call to report a validation error at all.

Execution Errors

Execution errors are failures that occur at runtime - either during command execution or property evaluation. These may result from hardware malfunctions, device-reported exceptions, or internal logic issues within the connector. The following sections show how to document such errors clearly and how to include meaningful diagnostic information to help users and clients respond appropriately.

SiLA Error Declarations

When you know a specific error might occur during execution, you should define it explicitly in the SiLA interface using the errors argument or in the Raises section of the docstring.

Technical Note: An exception listed in both the errors argument of the SiLA command/property and in the Raises section of the docstring will preferentially use the docstring representation of the error description.
from unitelabs.cdk import sila


class MyError(Exception):
    """My error description."""


class MyFeature(sila.Feature):
    @sila.UnobservableProperty()
    async def get_my_property(self) -> float:
        """
        My property value.

        Raises:
          ZeroDivisionError: Explanation for conditions under which it is raised,
            and how users can avoid and/or correct for the error.
        """

        return 5 / 0

    @sila.UnobservableCommand(errors=[MyError])
    async def my_command(self) -> None:
        """
        Execute my command.

        Raises:
          MyError: Explanation for conditions under which it is raised,
            and how users can avoid and/or correct for the error.
        """

        raise MyError()

Listing 2: Declaring known execution errors in the SiLA interface so that clients can anticipate and handle them proactively.

Note: Only execution errors should be included in a command or property's errors list or its docstring Raises section. Validation errors, even if raised at runtime, must not be listed - since they are not part of the SiLA feature definition and are not exposed in the interface at design time.

Defined Execution Errors

When an exception that is listed in the interface's errors declaration (or in Raises section of the method's docstring) is raised, the client receives a SiLA Defined Execution Error. This error includes a globally unique identifier along with an error message. The message is either the string passed when the exception was raised or, if no message was provided, the docstring of the exception class. Because the error is explicitly defined in the SiLA interface, clients and users can anticipate and handle it in advance.

Undefined Execution Errors

When an exception is raised that is not declared in the interface's errors list, the client receives a SiLA Undefined Execution Error. The message follows the same rules as for defined errors, but no unique identifier is included - only the message is transmitted. Since these errors are not part of the design-time interface, they cannot be anticipated by clients.

Runtime Error Messages

Design-time error descriptions should help users understand why an error might occur and how to avoid it. At runtime, always raise errors with specific and actionable messages.

In the following example, the MyError exception is declared in the SiLA interface for my_command. This means that at design time, the client is already aware that MyError may occur and receives the description "Docstring description." as guidance for how to handle it. When the error is actually raised at runtime, a more detailed and situation-specific message is provided - "My error occurred and this is how you solve it: ..." - which is then passed to the client to aid in troubleshooting.

from unitelabs.cdk import sila


class MyError(Exception):
    """My error description."""


class MyFeature(sila.Feature):
    @sila.UnobservableCommand()
    async def my_command(self) -> None:
        """
        Description of my command.

        Raises:
          MyError: Docstring description.
        """

        raise MyError("My error occurred and this is how you solve it: ...")

Listing 3: Raising meaningful error messages at runtime to help users understand the cause and resolution of a failure.

Providing clear, actionable error messages both in the interface definition and at runtime improves usability and reduces support overhead.

Last updated