Card 06/ 08
A Request That Falls Off the End of the Chain
No handler accepts it, nobody raises an error, and the request silently does nothing.
When no handler in a chain accepts a request, the last link has nowhere to send it, and in the usual implementation it returns nothing. The caller receives an empty result instead of an error, and carries on. This card shows a request going unhandled, and how to end every chain with an answer.
A shop books parcels with the first carrier that can take their weight: a courier up to 2 kg, a van up to 30 kg, and freight up to 1,000 kg. The last carrier returns None when there is no next link.
def book(self, weight_kg: float) -> str | None:
if weight_kg <= self.max_kg:
return self.name
if self.next is not None:
return self.next.book(weight_kg)
return NoneA customer orders a 1,500 kg pallet.
What does the shop print for the pallet? Decide, then look.
class Carrier:
def __init__(self, name: str, max_kg: float) -> None:
self.name = name
self.max_kg = max_kg
self.next: Carrier | None = None
def book(self, weight_kg: float) -> str | None:
if weight_kg <= self.max_kg:
return self.name
if self.next is not None:
return self.next.book(weight_kg)
return None
courier = Carrier("courier", 2)
courier.next = van = Carrier("van", 30)
van.next = Carrier("freight", 1_000)
for weight in (1.5, 400, 1_500):
print(f"{weight} kg booked with: {courier.book(weight)}")1.5 kg booked with: courier
400 kg booked with: freight
1500 kg booked with: NoneIt prints "booked with: None". No carrier took the parcel, nothing raised an error, and the order goes on to the next step with no carrier attached.
The cost is a failure found later and further away: at the warehouse, when nobody arrives to collect the pallet. The chain did exactly what it was written to do, and nobody had decided what it should do with a request it could not handle.
The fix is to decide. Either end the chain with a default handler that accepts anything, or make the last link raise an error. This version does both: ManualQuote accepts every weight and asks for a quote, and a chain built without it raises LookupError rather than returning None.
class Carrier:
def __init__(self, name: str, max_kg: float) -> None:
self.name = name
self.max_kg = max_kg
self.next: Carrier | None = None
def book(self, weight_kg: float) -> str:
if weight_kg <= self.max_kg:
return self.name
if self.next is None:
raise LookupError(f"no carrier set up for {weight_kg} kg")
return self.next.book(weight_kg)
class ManualQuote(Carrier):
def __init__(self) -> None:
super().__init__("manual quote", float("inf"))
def book(self, weight_kg: float) -> str:
return f"manual quote requested for {weight_kg} kg"
courier = Carrier("courier", 2)
courier.next = van = Carrier("van", 30)
van.next = freight = Carrier("freight", 1_000)
freight.next = ManualQuote()
for weight in (1.5, 400, 1_500):
print(f"{weight} kg booked with: {courier.book(weight)}")1.5 kg booked with: courier
400 kg booked with: freight
1500 kg booked with: manual quote requested for 1500 kgThe default handler is the usual choice when an unhandled request has a sensible answer, as with the 404 a web framework sends. Raising is the choice when no answer is sensible and the failure should stop the caller at once.
← → move through sections and cards