Skip to main content
Appendix · Going the extra mile The motivating scenario: increment() first calls Receiver.receive(), then follows up with a transfer(). If the receive fails, you must not transfer. If the transfer fails after a successful receive, you may need to refund or roll back. That coupling — conditioning a follow-up call on the success of a prior one — is what composable error handling is about. The “To Panic is Human” chapter handles single-call failure on the transfer itself. This appendix is the next layer: multi-call flows where the second call depends on the first.

Two paths: .await and .spawn()

The straightforward path: wait for each call, branch on success/failure.
#[owner] // with confirmation
pub async fn increment(&mut self, to: Receiver) -> Result<()> {
    let receive_future = to.remote.receive();
    if let Err(increment_result) = receive_future.await {
        // receive failed — handle and bail
        return self.handle_increment_failure(increment_result);
    } else {
        // receive succeeded — now do the dependent transfer.
        // Cast `to` into a holder-typed id (see "The Client is Always Right"):
        let receiver_holder_id = GvmComponentId::new(
            *to.component_entity_id(),
            GvmComponentHeader::builder()
                .version(*to.header().version())
                .index(*to.header().index())
                .contract(*self.incrementer_simple_token_holder.header().contract())
                .component_code_id(
                    *self.incrementer_simple_token_holder.header().component_code_id()
                )
                .build()
        );

        let transfer_future = self.incrementer_simple_token_holder.remote.transfer(
            AmountInSubunits(1),
            Holder::new(receiver_holder_id)?,
            with_confirmation,
        );

        if let Err(transfer_error) = transfer_future.await {
            return self.handle_transfer_failure(transfer_error);
        }
        // else: it worked. Maybe celebrate.
    }
    Ok(())
}

#[private]
pub fn handle_increment_failure(&mut self, delivery_error: GvmError) -> Result<()> {
    // You can retry, or just propagate the error downstream.
    Err(delivery_error.into())
}

#[private]
pub fn handle_transfer_failure(&mut self, transfer_error: GvmError) -> Result<()> {
    // Retry, build & send a decrement, or propagate.
    Err(transfer_error.into())
}
Pros: linear, debuggable, reads top-to-bottom. The current activation waits for the cross-entity round trip.Cons: the entity lock is held longer (until both futures resolve).

When to use which

  • .await — when the multi-call chain is logically one operation that the caller treats as atomic-ish. Easier to write and debug.
  • .spawn() — when the entity should remain available for other activations while the chain finishes. Worth the extra state when the contract is hot.
The GvmComponentId::new(...) cast in both tabs above is the same idiom covered in The Client is Always Right — adopt that style and the cast disappears (you receive a bare GvmComponentId from the start).