Null Propagation
How missing data flows through a project without crashing anything.
In traditional code, missing data is an exception. Something throws, a stack unwinds, you write try/catch to contain the mess. Weft takes a different path: missing data is normal, and the language has a built-in rule for how it flows.
The rule: a node with a required input that receives null does not run. It skips, and all of its outputs produce null. Every downstream node with a required input reading those nulls also skips. Null cascades through the graph until it hits a port that explicitly opts into receiving it.
Required vs optional, one more time
A port is required by default. To make it optional, add ? after the type:
- Required input receives null: the node skips. Its outputs are all null.
- Optional input receives null: the node runs. The input is null inside the node's code, and the node must handle that case.
Optional is an explicit signal that the node knows how to deal with absence. Required is the safe default: if data is missing, stop here instead of running with garbage.
Why this is useful
Null propagation is not just a convenience. It turns out to be the mechanism underneath three different features:
- Branching. A router outputs null on inactive branches. Each branch's downstream nodes have required inputs, so they skip. Only the active branch runs. No
ifstatements, no dead code, no visited tracking. - Failure containment. If an API call fails and its output node declares a non-
Nulltype, the node produces null and everything downstream of it skips. The project does not crash. Other independent branches keep running. - Parallel fault tolerance. In a list of 100 items processed in parallel, if 3 lanes fail, only those 3 lanes produce null. The other 97 continue. The gathered result is
List[T | Null]and downstream code filters or handles the nulls.
Three features that would normally need three separate mechanisms, handled by one rule.
Skipped is not failed
A skipped node is not an error. It is a first-class runtime state: "the preconditions for this node were not met, so it did not run". In the execution view, skipped nodes are gray. Failed nodes are red. You can tell at a glance whether your branching worked the way you intended or whether something actually went wrong.
When to make a port optional
Mark a port optional when the node's behavior with a null value is intentional:
- A
Templatethat produces a partial render when some fields are missing. - A
Packthat tolerates missing keys. - An
ExecPythonnode that checks every optional input and returns a sensible result when some are missing. - A downstream aggregator that reads a
List[T | Null]and explicitly filters the nulls.
When in doubt, leave it required. Weft's default is to stop early and skip gracefully, because that is what you want most of the time.
Do not over-type with Null
A common mistake is declaring ports as String | Null everywhere "just in case". Do not.
If an upstream node can skip, it produces null, but that null never actually reaches a downstream required port, because the downstream node also skips. You do not need to type around nulls that will never show up.
Only add | Null or ? when the upstream node can genuinely produce null as a valid value, not as a skip signal. Examples: an optional search result, a field that is sometimes missing from API data, a list that may legitimately be empty.
@require_one_of
One edge case: a node with all-optional inputs. By default it runs even when every input is null, which is almost never what you want. Use @require_one_of to say "at least one of these must be non-null, otherwise skip":
This gives you "optional, but at least one required", without forcing you to mark a specific one as required.
What's next
- Branching: the most common use of null propagation.
- Parallel processing: null lanes in gathered results.
- Executions: skipped nodes in the run view.