Frontend errors are raised from the resource tree and the trackers, before the backend is called. They are portable: the same mistake raises the same class on every backend.
Backend errors come from the machine and are vendor-specific — STARFirmwareError, TecanError, VantageFirmwareError. Of the 137 exception classes in 0.2.2, about 60 belong to the STAR backend.
Volume tracking is off by default (chapter 1). The recipes below need it on.
12.1 The portable errors
Look up which error a given mistake raises, and what kind of error it is.
The classes sort into four groups by where they are raised, which also determines whether they are portable and whether a tracking flag can suppress them.
Tree errors — raised by the resource tree, from a name or a location that does not resolve.
Error
Raised at
Message
ResourceNotFoundError
resources/deck.py, resources/resource.py
Resource 'x' not found (deck) / Resource with name 'x' does not exist. (resource)
NoLocationError
resources/resource.py
Resource 'x' has no location.
Tracker errors — raised by the volume and tip trackers, from the model’s record of what is where. These are the only ones the no_*_tracking() hatches suppress (below).
Error
Raised at
Raised when
TooLittleLiquidError
resources/volume_tracker.py
aspirating more than the container holds
TooLittleVolumeError
resources/volume_tracker.py
dispensing more than it can take
NoTipError
resources/tip_tracker.py
a tip was expected, none found
HasTipError
resources/tip_tracker.py
the tip spot already holds a tip
Frontend guards — raised by LiquidHandler itself while planning, before the backend is called. Not suppressible.
Error
Raised at
Raised when
HasTipError
liquid_handling/liquid_handler.py
the channel already has a tip
BlowOutVolumeError
liquid_handling/liquid_handler.py
blow-out air exceeds the aspirated volume
ChannelsDoNotFitError
liquid_handling/channel_positioning.py
channels cannot be spaced in the resource
Backend-raised — these two live in shared modules but are constructed only inside one backend, so they do not appear on other machines.
Two further classes are defined and never raised anywhere in 0.2.2: CrossContaminationError (below) and ResourceDefinitionIncompleteError.
from pylabrobot.resources.errors import ResourceNotFoundError, NoLocationErrortry: lh.deck.get_resource("no_such_plate")except ResourceNotFoundError as e:print("ResourceNotFoundError:", e)orphan = cor_96_wellplate_360uL_Fb(name="orphan")try: orphan.get_absolute_location()except NoLocationError as e:print("NoLocationError:", e)
1
Constructed but never assigned, so it has no position in the tree.
ResourceNotFoundError: Resource 'no_such_plate' not found
NoLocationError: Resource 'orphan' has no location.
Backend errors.
Everything not in the table is vendor-specific: STARModuleError and its ~60 subclasses, TecanError, CytomatBusyError, and the Liconic and Molecular Devices families. Catch these by base class (except STARFirmwareError), not by leaf. Chapter 12 covers backend error families.
See also:ResourceNotFoundError is the failure mode of get_resource in chapter 2; ChannelsDoNotFitError is the no-go-zone case in chapter 4.
12.2 Continue a worklist past a bad row
Run a list of transfers where some rows fail, and complete the rest.
print("completed:", len(worklist) -len(failed), "of", len(worklist))for row in failed:print(" failed:", row)
The fourth would bring B1 to 400 µL. The aspirate succeeds and the dispense raises TooLittleVolumeError, which leaves 100 µL in the tip.
4
Aspirating from empty A2 raises TooLittleLiquidError on the aspirate, so no dispense is attempted.
5
Catching the two volume errors specifically leaves other exceptions to propagate.
Gotcha: a tip holding liquid cannot be dropped.
If an aspirate succeeds and its dispense fails, the tip keeps the liquid, and drop_tips raises:
RuntimeError: Cannot drop tip with volume 50.0
This is a bare RuntimeError, not one of the portable classes, so except TooLittleVolumeError does not catch it. Dispense to waste before dropping, or handle the drop separately.
Python you need here
except (A, B) as e catches a tuple of classes in one clause. Python takes the first matching except, so list subclasses before their parents.
See also: the same rows checked before the run in chapter 7.
12.3 Read a partial multichannel failure
Determine which channels failed and which succeeded after a multichannel operation raises.
from pylabrobot.liquid_handling.errors import ChannelizedErrorfrom pylabrobot.resources.errors import NoTipError, TooLittleLiquidErrorerror = ChannelizedError(errors={2: NoTipError("Channel 2 does not have a tip."),5: TooLittleLiquidError("Not enough liquid in container: 50.0uL > 0uL."),})used_channels =list(range(8))survivors = [ch for ch in used_channels if ch notin error.errors]print("failed channels: ", sorted(error.errors))print("survived: ", survivors)print("failure count: ", len(error))for channel, cause insorted(error.errors.items()):print(f" channel {channel}: {type(cause).__name__} - {cause}")
1
Constructed here, not provoked: the chatterbox does not raise one (see below).
2
error.errors is a Dict[int, Exception] keyed by channel index. Channels absent from it succeeded.
3
ChannelizedError implements __len__.
4
Each value is that channel’s individual error.
failed channels: [2, 5]
survived: [0, 1, 3, 4, 6, 7]
failure count: 2
channel 2: NoTipError - Channel 2 does not have a tip.
channel 5: TooLittleLiquidError - Not enough liquid in container: 50.0uL > 0uL.
Gotcha: only the STAR backend raises this.
In 0.2.2, ChannelizedError is constructed in one place — liquid_handling/backends/hamilton/STAR_backend.py. The chatterbox never produces one, so a partial-failure handler cannot be exercised in simulation. The frontend logic that consumes it is backend-agnostic.
See also:lh.probe_tip_presence_via_pickup() catches ChannelizedError, marks the failed channels’ spots as empty, and re-raises if a channel it did not ask about appears in errors.
12.4 Suppress tracking for a block
Aspirate from a well whose contents PLR does not know about, without disabling tracking for the rest of the run.
D1 was never filled. With tracking on this raises TooLittleLiquidError.
3
Restored when the block exits normally — but see the gotcha below.
WarningGotcha: an exception inside the block leaves tracking off
Neither context manager wraps its yield in try/finally:
@contextlib.contextmanagerdef no_volume_tracking(): old_value = this.volume_tracking_enabled this.volume_tracking_enabled =Falseyield# <-- an exception here skips the line below this.volume_tracking_enabled = old_value
If the body raises, the restore never runs and volume tracking stays off for the rest of the session, including in code that never asked to suppress it:
try:with no_volume_tracking():raiseValueError("something failed mid-block")exceptValueError:passprint("volume tracking still on?", does_volume_tracking())set_volume_tracking(True) # put it back for the rest of this chapter
volume tracking still on? False
no_tip_tracking() has the same shape and the same behaviour. To keep the suppression scoped, restore it yourself:
from pylabrobot.resources import set_volume_trackingwas_on = does_volume_tracking()try:with no_volume_tracking(): ...finally: set_volume_tracking(was_on)
What the tracker knows.
A container’s tracked volume is only what PLR was told — by set_volume(), or by an aspirate or dispense it performed. Liquid that arrived any other way is not in the model: a trough filled by hand before the run, a plate returned from a machine PLR does not model, or a well whose starting volume was never seeded. In each case the tracked volume is 0 and an aspirate raises TooLittleLiquidError while the well is physically full.
WarningGotcha: no_tip_tracking() does not suppress every HasTipError
There are two, and only one is suppressible:
Message
Raised by
Suppressible
"Channel has tip"
liquid_handler.py, on the channel head
no
"<spot> already has a tip."
the tip spot’s tracker
yes
In pick_up_tips, the channel-head check sits one line above the does_tip_tracking() guard, so it runs unconditionally. Picking up a second tip on an occupied channel fails inside no_tip_tracking() exactly as it does outside.
See also:set_volume_tracking(True) / set_tip_tracking(True) set the flags globally, as this chapter’s setup does; the no_* context managers scope the suppression instead.
12.5CrossContaminationError is never raised
CrossContaminationError imports and is never raised.
import warningsfrom pylabrobot.resources.errors import CrossContaminationErrorwith warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") CrossContaminationError("tip touched two liquids")print(caught[0].category.__name__, "->", caught[0].message)
1
Constructing one emits a DeprecationWarning from its __init__. Nothing in the package raises it.
DeprecationWarning -> Cross contamination tracking is deprecated and will be removed in a future version.
Related removals.
set_cross_contamination_tracking() and tracker.liquid_history are gone entirely in 0.2.2. Code from older PLR that calls them raises ImportError or AttributeError at the import line.
12.6 What to remember
Frontend errors are portable; backend errors are not. Eight classes cover the portable liquid-handling surface, in three groups: tree, tracker, and frontend guard. Only the tracker group is suppressible.
ChannelizedError (STAR) and NoChannelError (Opentrons) sit in shared modules but are raised by one backend each. CrossContaminationError and ResourceDefinitionIncompleteError are never raised at all.
A caught frontend error means nothing moved — trackers rolled back, the loop can continue.
ChannelizedError.errors maps channel → cause; channels not listed succeeded and are already committed. Only the STAR backend raises it.
no_volume_tracking() / no_tip_tracking() are scoped context managers.
"Channel has tip" is unconditional; the tip-spot HasTipError is suppressible.