Skip to content

fujilib.errors

The typed exception hierarchy and ErrorContext (design §9).

fujilib.errors

Typed exception hierarchy for :mod:fujilib (design §9).

Every library exception inherits from :class:FujiError and carries a structured :class:ErrorContext. The message is the human-readable summary; the context is the machine-readable detail (command, protocol, port, station address, channel, register and function code, request/response bytes, elapsed time).

The pattern matches the *lib family: ErrorContext is a frozen dataclass(slots=True) whose extra mapping is always frozen into a read-only :class:types.MappingProxyType, and :meth:FujiError.with_context does a slot-safe copy so an inner layer can raise and an outer layer enrich.

anymodbus exceptions are translated to this hierarchy at a single boundary in the Modbus client, always with raise ... from exc (design §4.6).

Retrying. Each class states whether retrying the same call can help. The library itself retries only reads (design §4.5); writes and operation commands are never retried automatically, because a lost reply does not mean the write failed (design §6.4).

MRO caution. Where a class belongs to two branches (a Modbus timeout is also a transport timeout; a missing sink extra is also a configuration problem), the extra base is a plain marker: every class shares the single :meth:FujiError.__init__ and none defines __slots__, so the MRO stays unambiguous and :meth:~FujiError.with_context round-trips through it.

ErrorContext dataclass

ErrorContext(
    command_name=None,
    protocol=None,
    port=None,
    address=None,
    channel=None,
    register_address=None,
    function_code=None,
    request=None,
    response=None,
    elapsed_s=None,
    extra=_empty_extra(),
)

Structured context attached to every :class:FujiError.

Fields are best-effort — missing data is None rather than raising.

protocol and channel are typed as str so they accept the library's ProtocolKind and ChannelId string enums as well as plain strings. register_address is the 0-based on-the-wire address, not the 30001/40001-based register number.

extra accepts any Mapping and is always frozen into a read-only :class:types.MappingProxyType at construction so the shared empty sentinel can never be mutated through error.context.extra[k] = v.

merged

merged(**updates)

Return a new context with updates overlaid. Unknown keys go to extra.

Source code in src/fujilib/errors.py
def merged(self, **updates: Any) -> Self:
    """Return a new context with ``updates`` overlaid. Unknown keys go to ``extra``."""
    known: dict[str, Any] = {}
    extra_updates: dict[str, Any] = {}
    for key, value in updates.items():
        if key in _CONTEXT_KNOWN_FIELDS:
            known[key] = value
        else:
            extra_updates[key] = value

    new_extra: Mapping[str, Any] = (
        MappingProxyType({**self.extra, **extra_updates}) if extra_updates else self.extra
    )
    return replace(self, **known, extra=new_extra)

FujiAnalyzerStateError

FujiAnalyzerStateError(message='', *, context=None)

Bases: FujiError

What the analyzer is doing forbids the write or command now; nothing was written.

Raised after reading the analyzer's status and before any write (design §6.1): a calibration is running, the front panel shows a menu, or the analyzer reports an error that makes a calibration meaningless. Retryable once the analyzer is back on the measurement screen.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiCapabilityError

FujiCapabilityError(message='', *, context=None)

Bases: FujiError

The operation is not available on this analyzer, model or option.

Raised before any I/O once a capability is known to be UNSUPPORTED (design §6.6). Not retryable until reprobe().

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiConfigurationError

FujiConfigurationError(message='', *, context=None)

Bases: FujiError

Configuration-level error: bad arguments or conflicting settings.

Not retryable: the call itself has to change. A ValueError escaping anymodbus is a fujilib bug and also surfaces here (design §4.4).

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiConfirmationRequiredError

FujiConfirmationRequiredError(message='', *, context=None)

Bases: FujiConfigurationError

An operation above SafetyTier.READ_ONLY was attempted without confirm=True.

Raised before any I/O (design §6.1). Retry only by passing confirm=True deliberately.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiConnectionError

FujiConnectionError(message='', *, context=None)

Bases: FujiTransportError

The port could not be opened, or the connection was lost.

Retryable only after the device is reopened.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiDecodeError

FujiDecodeError(message='', *, context=None)

Bases: FujiProtocolError

A register value was received intact but does not decode.

Examples: a BCD digit above 9, an enum value the manual does not define. Not retryable. Distinct from :class:FujiConfigurationError (design §4.4).

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiError

FujiError(message='', *, context=None)

Bases: Exception

Base class for every :mod:fujilib exception.

Carries a typed :class:ErrorContext. The message is the human-readable summary; the context is the machine-readable detail.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

with_context

with_context(**updates)

Return a copy of this error with its context updated.

Useful when an inner layer raises and an outer layer wants to enrich the context (for instance adding port or elapsed_s).

Source code in src/fujilib/errors.py
def with_context(self, **updates: Any) -> Self:
    """Return a copy of this error with its context updated.

    Useful when an inner layer raises and an outer layer wants to enrich
    the context (for instance adding ``port`` or ``elapsed_s``).
    """
    cls = type(self)
    new = cls.__new__(cls)
    new.args = self.args
    try:
        new.__dict__.update(self.__dict__)
    except AttributeError:  # pragma: no cover — no slotted subclass today
        for slot in getattr(cls, "__slots__", ()):
            if hasattr(self, slot):
                object.__setattr__(new, slot, getattr(self, slot))
    new.context = self.context.merged(**updates)
    new.__cause__ = self.__cause__
    new.__context__ = self.__context__
    new.__traceback__ = self.__traceback__
    return new

FujiFirmwareError

FujiFirmwareError(message='', *, context=None)

Bases: FujiCapabilityError

The operation needs firmware the analyzer does not have.

For example, the calibration log needs firmware 2.24 or later. Not retryable.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiFrameError

FujiFrameError(message='', *, context=None)

Bases: FujiProtocolError

Bad CRC, wrong length or malformed framing.

Usually link noise. Reads are retried by the client; writes are not.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiModbusError

FujiModbusError(message='', *, context=None)

Bases: FujiProtocolError

A Modbus exception response or other Modbus-layer failure.

__cause__ preserves the original anymodbus exception.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiModbusIllegalDataAddressError

FujiModbusIllegalDataAddressError(
    message="", *, context=None
)

Bases: FujiModbusError, FujiProtocolUnsupportedError

Modbus exception 02 — the address is outside the analyzer's map.

For a well-formed capability probe this marks the capability UNSUPPORTED (design §6.6). Not retryable.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiModbusIllegalDataValueError

FujiModbusIllegalDataValueError(
    message="", *, context=None
)

Bases: FujiModbusError

Modbus exception 03 — illegal data value.

The manual describes it as "too many words requested / outside the map"; the bench unit also returns it for a read that crosses a region end (design §2.2). Not retryable.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiModbusIllegalFunctionError

FujiModbusIllegalFunctionError(message='', *, context=None)

Bases: FujiModbusError, FujiProtocolUnsupportedError

Modbus exception 01 — the function code is not implemented.

The bench unit answers FC01 and FC02 with exception 02 instead (design §2.2). Not retryable.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiModbusTimeoutError

FujiModbusTimeoutError(message='', *, context=None)

Bases: FujiModbusError, FujiTimeoutError

No reply arrived within the request timeout.

A bad CRC, a wrong station number or an over-long inter-byte gap all produce no reply (design §2.2). Reads are retried by the client and each retry is counted (design §4.5); writes are not.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiProtocolError

FujiProtocolError(message='', *, context=None)

Bases: FujiError

Protocol-level error: framing, decoding, or an unexpected reply.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiProtocolUnsupportedError

FujiProtocolUnsupportedError(message='', *, context=None)

Bases: FujiProtocolError

The analyzer does not support this request at all. Not retryable.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiResyncRequiredError

FujiResyncRequiredError(message='', *, context=None)

Bases: FujiTransportError

The port is still discarding a possible late reply after a cancellation.

New requests are refused until resynchronization completes (design §4.2); retry after it has.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiSinkDependencyError

FujiSinkDependencyError(message='', *, context=None)

Bases: FujiSinkError, FujiConfigurationError

A sink's optional backing library is not installed.

Also a :class:FujiConfigurationError, because a missing extra is a configuration problem from the caller's point of view. Not retryable until the extra is installed.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiSinkError

FujiSinkError(message='', *, context=None)

Bases: FujiError

Base class for errors raised by sinks.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiSinkSchemaError

FujiSinkSchemaError(message='', *, context=None)

Bases: FujiSinkError

A batch's shape is incompatible with the sink's locked schema. Not retryable.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiSinkWriteError

FujiSinkWriteError(message='', *, context=None)

Bases: FujiSinkError

The backing store rejected a write.

__cause__ preserves the backend's exception; whether a retry can help depends on it.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiTimeoutError

FujiTimeoutError(message='', *, context=None)

Bases: FujiTransportError

A transaction timed out, or a per-call operation deadline expired.

For a deadline, the context carries the operation name and the elapsed time (design §4.6, §6.4). Reads may be retried. A write that timed out raises :class:FujiWriteOutcomeUnknownError instead.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiTransportError

FujiTransportError(message='', *, context=None)

Bases: FujiError

I/O-layer error from the serial transport or the Modbus bus.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiValidationError

FujiValidationError(message='', *, context=None)

Bases: FujiConfigurationError

A request failed validation before any I/O.

Examples: an unknown parameter name, a channel or range that does not exist, a value outside its raw limits. Not retryable.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiVerificationError

FujiVerificationError(message='', *, context=None)

Bases: FujiProtocolError

The read-back after a write did not match the value written.

The context carries the expected and observed values. Not retryable: the front panel may have changed the setting in between (design §6.3).

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT

FujiWriteOutcomeUnknownError

FujiWriteOutcomeUnknownError(message='', *, context=None)

Bases: FujiTimeoutError

A write request was sent but its outcome could not be established.

The analyzer may have applied the write even though the reply was lost (design §6.4). Not retryable: read the setting back and decide. The context's extra records the write_state, whether transmission started, and any observed value.

Source code in src/fujilib/errors.py
def __init__(self, message: str = "", *, context: ErrorContext | None = None) -> None:
    super().__init__(message)
    self.context = context if context is not None else _EMPTY_CONTEXT