Austin Weisgrau
12/04/2025, 12:35 AMMarvin
12/04/2025, 12:35 AMMarvin
12/04/2025, 12:42 AMfuture.result() (default raise_on_failure=True) → raises the task’s exception.
- You pass a future as an argument to another Prefect task/flow (not wrapped in allow_failure) → Prefect implicitly resolves it before running the downstream task and raises UpstreamTaskError if it failed.
- The flow returns a value that contains futures → Prefect resolves futures to states and aggregates; failed states will fail the flow and raise on result retrieval for the flow.
When it will not surface
- You never call .result() or otherwise use the future in a context where Prefect resolves it.
- You call future.wait() (waits but does not raise).
- You pass futures around inside custom objects (e.g., dataclass/DTO) that Prefect does not inspect/unwrap — resolution is skipped so downstream tasks can “get past” them.
- You’ve annotated with allow_failure(...), which allows failed upstreams to be passed downstream without raising.
This explains your report: “it’s letting other tasks get past that future without raising” usually means downstream code is seeing a PrefectFuture (or a custom container holding one), not the task’s result. Prefect won’t implicitly resolve/raise in ordinary Python control flow like if future: or when stashing the future inside a custom class.
How to get the earlier “bubble up” behavior
- Resolve immediately after mapping:
futures = my_task.map(items)
# Wait for completion (optional)
from prefect.futures import wait
wait(futures)
# Raise on any failure
for f in futures:
f.result() # raises on the first failed mapped item
- Or force resolution in one place:
from prefect.futures import resolve_futures_to_results
results = resolve_futures_to_results(futures) # raises if any failed
- Or ensure you pass futures directly as inputs to a downstream Prefect task (not wrapped in a custom object), so Prefect resolves them and raises an UpstreamTaskError automatically.
- Only use allow_failure(...) when you explicitly want to handle failed states downstream.
If this still doesn’t match what you’re seeing, can you share:
- Your Prefect version (3.x minor/patch)
- A minimal snippet that shows the mapped task failing and the downstream task “getting past” it
- Whether you’re passing the futures directly vs. inside a custom object, and whether allow_failure is used anywhere
With a small repro, I can pinpoint whether this is expected lazy resolution, a wrapping/container edge case, or something else.Austin Weisgrau
12/04/2025, 12:45 AMfrom prefect import flow, task
@task
def mytask(value: int):
if value == 1:
raise ValueError(value)
return value
@task
def mytask2(value: int):
return value
@flow
def myflow():
values = [i for i in range(3)]
results = mytask.map(values)
results2 = mytask2.map(results)
for result in results2:
print(result.result())
myflow()
it's not until line 21 (for result in results2) that we get an exception raised. shouldn't we have an exception raised before that?