Problem
tell() is documented as fire-and-forget and typed void, but it throws MailboxFullError into the sender when the target's mailbox is full under the reject overflow policy. When the sender is an actor, that throw escapes its onReceive and the sender is failed and restarted because the receiver was slow — failure propagates backwards along the message flow, which is the opposite of what supervision is for.
Compounding it: reject is the default for an explicitly constructed BoundedMailbox, while the cell's implicit one uses drop-head. The same class has two different defaults depending on who built it, so opting into a bounded mailbox by hand silently changes the failure mode from "lose messages" to "kill the sender".
Evidence
The throw:
src/mailbox/BoundedMailbox.ts:56-61
.with('drop-new', () => {
this.droppedCount++;
this.onDrop?.('drop-new');
})
.with('reject', () => { throw new MailboxFullError(this.capacity); })
.exhaustive();
The default that makes it reachable by accident:
src/mailbox/BoundedMailbox.ts:36
this.overflow = settings.overflow ?? 'reject';
versus the cell's implicit construction, which passes DEFAULT_MAILBOX_OVERFLOW ('drop-head') at src/internal/ActorCell.ts:194-198.
The contract it breaks:
src/ActorRef.ts:59
* forget; ask() provides a request/response Promise.
Reproduced. An actor floods a slow child whose mailbox is { capacity: 2, overflow: 'reject' }:
sender restarts caused by the RECEIVER being full: 11
And from outside an actor, where there is no supervisor to absorb it:
plain tell() from outside an actor threw: MailboxFullError
Proposal
tell() must not throw. Two coherent designs, either acceptable:
reject reports to the sender as a message, not as an exception — the target's deadLetters receives the envelope and the sender may watch for it. This keeps tell total.
reject is renamed and re-scoped to a non-tell entry point: a trySend(): boolean (or a promise that resolves when there is room) that a caller opts into explicitly, with tell never taking that path.
The second is more useful, because a backpressure-aware sibling to tell is what the I/O edge actually needs — today an HTTP request or broker message becomes a tell with no way to learn that it did not fit.
Separately, make BoundedMailbox's default overflow the same whichever way it is constructed.
Acceptance sketch
Verification status
Found in the ten-lens production-readiness review of 2026-08-05 (v0.13.0) and re-verified before filing: reproduced by execution for both the actor-sender and the plain-caller case.
Part of the production-readiness review batch — tracked in #913.
Problem
tell()is documented as fire-and-forget and typedvoid, but it throwsMailboxFullErrorinto the sender when the target's mailbox is full under therejectoverflow policy. When the sender is an actor, that throw escapes itsonReceiveand the sender is failed and restarted because the receiver was slow — failure propagates backwards along the message flow, which is the opposite of what supervision is for.Compounding it:
rejectis the default for an explicitly constructedBoundedMailbox, while the cell's implicit one usesdrop-head. The same class has two different defaults depending on who built it, so opting into a bounded mailbox by hand silently changes the failure mode from "lose messages" to "kill the sender".Evidence
The throw:
The default that makes it reachable by accident:
versus the cell's implicit construction, which passes
DEFAULT_MAILBOX_OVERFLOW('drop-head') atsrc/internal/ActorCell.ts:194-198.The contract it breaks:
Reproduced. An actor floods a slow child whose mailbox is
{ capacity: 2, overflow: 'reject' }:And from outside an actor, where there is no supervisor to absorb it:
Proposal
tell()must not throw. Two coherent designs, either acceptable:rejectreports to the sender as a message, not as an exception — the target'sdeadLettersreceives the envelope and the sender may watch for it. This keepstelltotal.rejectis renamed and re-scoped to a non-tellentry point: atrySend(): boolean(or a promise that resolves when there is room) that a caller opts into explicitly, withtellnever taking that path.The second is more useful, because a backpressure-aware sibling to
tellis what the I/O edge actually needs — today an HTTP request or broker message becomes atellwith no way to learn that it did not fit.Separately, make
BoundedMailbox's default overflow the same whichever way it is constructed.Acceptance sketch
tell()never throws, for any overflow policy.trySendresult.BoundedMailboxhas one default overflow policy regardless of construction site, documented.Verification status
Found in the ten-lens production-readiness review of 2026-08-05 (
v0.13.0) and re-verified before filing: reproduced by execution for both the actor-sender and the plain-caller case.Part of the production-readiness review batch — tracked in #913.