# When a child is cancelled or fails

> A workflow you waited for can end badly — cancelled, or failed. `catch` is how you handle it; not catching it is how you let it travel onward, and the run then ends at the `terminal cancel` or `terminal error` you declared. The two outcomes stay distinct however far they travel, so a workflow three levels up can still tell "the work below me was cancelled" from "it broke".

<!-- id: workflow-fault-propagation · area: workflow · stability: stable · html: https://osysharp.com/reference/workflow/fault-propagation/ -->

## Summary        {#summary}
`await Workflow.Run("child", child)` waits for a workflow to finish. If it finishes **badly** — at a `terminal cancel` or a
`terminal error` — that outcome arrives here as `WorkflowCancelled` or `WorkflowError`.

You have exactly two choices, and both are things you say in the code:

- **`catch` it** — cancellation or failure stops here, and you decide what happens next.
- **Don't** — it keeps going. This run ends at *its own* `terminal cancel` / `terminal error`, and whoever is waiting
  on *this* run receives the same kind in turn.

Not catching is a real choice, not an oversight. "If the work I'm waiting on is cancelled, I'm cancelled" is usually
exactly right, and it is the same thing C# does with a cancellation that reaches the top of a task.

## Signature      {#signature}
```osy syntax
try { await Workflow.Run("child", child); goto Done; }
catch (WorkflowCancelled e) { goto Withdrawn; }   // handle it — it stops here
catch (WorkflowError e)     { goto Rejected; }

terminal cancel Withdrawn { Message = "…"; }      // …or declare where an UNCAUGHT one lands
terminal error  Rejected  { Message = "…"; }
```

`e.Message` is the terminal's own `Message` — the sentence the workflow that ended wrote about why.

## Description    {#description}

### The two kinds mean different things, and the difference survives the trip   {#two-kinds}
`WorkflowCancelled` is a **deliberate stop**: the work was called off. `WorkflowError` is a **failure**: it went wrong.
Keeping them apart is the whole reason there are two, and it matters most far from where it happened — an approval
three workflows up should react differently to "the customer withdrew" than to "the payment provider rejected it".

So the kind is preserved every step of the way. A cancellation that travels up four runs is still a cancellation when
it gets there, and the fourth run's `catch (WorkflowCancelled e)` is what fires.

### Where an uncaught one lands, and why you have to say   {#uncaught}
An uncaught outcome ends the run at the terminal you declared for it — a **real transition**: the tracked property
moves, the audit records it, and the terminal's `Message` is what the next run up receives.

That is why the compiler asks you to declare one. If a wait can be cancelled and nothing catches it, the run is going
to end there, and a run has to end *somewhere you named*. The rule is narrow on purpose — you are only asked when
**both** are true:

- nothing catches it on that path, **and**
- the workflow you are waiting on can actually produce that outcome.

A child that declares no `terminal cancel` can never be cancelled, so you are never asked to declare a cancel terminal
for it. You will not be made to write states that can't be reached.

Declaring **two** terminals of the same kind is also an error: an uncaught outcome would have two places to land, and
the compiler will not guess. Keep one, or catch it and `goto` the one you mean.

### You cannot raise these yourself   {#platform-only}
`WorkflowCancelled` and `WorkflowError` are raised by the platform, and only by the platform. `throw`ing one is a
compile error.

They mean one precise thing — *a workflow I waited for ended at a declared terminal* — and everything above depends on
that staying true. To end **your own** workflow that way, `goto` its terminal: that is declared, audited, moves the
tracked property, and carries a `Message`. To fail an ordinary function, `throw new Exception("…")`.

## Examples       {#examples}
A child that can end all three ways, and a parent that handles one of them and lets the other travel:

```osy title="the workflows" test app=wf-fault-propagation-example
enum OrderStatus { Placed, Shipped, Rejected, Withdrawn }
enum PickStatus  { Waiting, Picked, Empty, Cancelled }

entity Order { [Required, MaxLength(40)] string Reference; OrderStatus Status = OrderStatus.Placed; }
entity Pick  { [Required] Order Order; PickStatus Status = PickStatus.Waiting; }

workflow PickFlow {
  Tracks = Pick.Status; Autostart = false; Initial = Waiting;
  event Report(bool inStock);
  state Waiting {
    subscribe Report(bool inStock);
    on Report(bool inStock) {
      when (inStock) { goto Picked; }
      default { goto Empty; }
    }
  }
  terminal success Picked    { }
  terminal error   Empty     { Message = "out of stock"; }
  terminal cancel  Cancelled { Message = "picking called off"; }
}

workflow OrderFlow {
  Tracks = Order.Status; Autostart = false; Initial = Placed;
  state Placed {
    enter {
      var pick = Workflow.Once("make-pick", () => new Pick { Order = this.Item });
      try { await Workflow.Run("pick", pick); goto Shipped; }
      catch (WorkflowError e) { goto Rejected; }     // a FAILURE stops here — we reject the order
    }
  }
  terminal success Shipped   { }
  terminal success Rejected  { }
  // A CANCELLATION is deliberately not caught: if picking was called off, the order is withdrawn. This is the
  // terminal that says where it lands — and without it, the code above would not compile.
  terminal cancel  Withdrawn { Message = "order withdrawn"; }
}
```

Read the `Placed` state as a sentence: *ship it if the pick succeeds; reject it if the pick fails; if the pick is
called off, so is the order.* The third clause is the terminal, not a `catch`.

## Notes          {#notes}
- **Only these two outcomes behave this way.** An ordinary failure — a dropped connection, a timeout, a bug — is not a
  workflow outcome, and the platform retries it rather than ending the run, because another attempt may well succeed.
  A cancelled or failed child is different: it has already finished, so trying again would read the same answer.
- **Compensations still run.** If the run had a [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) scope open, leaving through an uncaught outcome
  disposes it on the way out, so the work it registered is undone before the run ends.
- **A `catch (Exception e)` catches these too**, like any other. Reach for the specific ones when the difference
  between "called off" and "went wrong" changes what you do.

## See also       {#see-also}
- [Workflow.Run (start a workflow)](https://osysharp.com/reference/workflow/run/) — start a workflow, and optionally wait for it.
- [Workflow.BeginSaga (a compensating saga scope)](https://osysharp.com/reference/workflow/beginsaga/) — undoing work when a wait ends badly.
- [Parallel legs (start several, then wait for them)](https://osysharp.com/reference/workflow/parallel-legs/) — waiting on several at once, and what happens when one of them fails.
