Skip to content

feat(execution): expose partial result on abort errors#4674

Merged
yaacovCR merged 1 commit into
graphql:17.x.xfrom
yaacovCR:expose-partial-result-on-abort
Apr 22, 2026
Merged

feat(execution): expose partial result on abort errors#4674
yaacovCR merged 1 commit into
graphql:17.x.xfrom
yaacovCR:expose-partial-result-on-abort

Conversation

@yaacovCR
Copy link
Copy Markdown
Contributor

@yaacovCR yaacovCR commented Apr 21, 2026

When execution is aborted while work is still resolving, the executor previously rejected with only the abort reason. That loses access to any response data and errors that execution can still produce while unwinding the in-flight operation.

This PR wraps pre-result aborts in AbortedGraphQLExecutionError and attach the partial result as abortedResult. If the response has already been produced, abortedResult is that response value. If root execution is still pending, abortedResult is the original execution promise so callers can await the eventual partial response separately from the abort rejection.

For example, if a query aborts while a field resolver is pending, execute() rejects with the original abort reason as the error message and cause, while error.abortedResult later resolves to a response such as { data: { blocker: null }, errors: [{ message: 'Aborted!', path: ['blocker'] }] }.

[Incremental execution follows the same pattern for the pending initial result, but aborts after the initial incremental payload continue to surface as rejection of next calls on the subsequentResults iterator (without this new wrapper).
abortedResult is usually a a Promise rather than a value because since the execute promise rejects immediately, execution has not finished.]

It may sometimes be a value, i.e. if the executor aborted internally during synchronous execution, which currently can happen if a resolver triggers the external abort signal or if there is a developer error, such as using a schema or query with defer/stream directives when using execute (instead of using experimentalExecuteIncrementally).

@phryneas => Was this your original suggestion on Discord at some point?

@yaacovCR yaacovCR added the PR: feature 🚀 requires increase of "minor" version number label Apr 21, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 21, 2026

@yaacovCR is attempting to deploy a commit to the The GraphQL Foundation Team on Vercel.

A member of the Team first needs to authorize it.

When execution is aborted while work is still resolving, the executor previously rejected with only the abort reason. That loses access to any response data and errors that execution can still produce while unwinding the in-flight operation.

This PR wraps pre-result aborts in `AbortedGraphQLExecutionError` and attach the partial result as `abortedResult`. If the response has already been produced, `abortedResult` is that response value. If root execution is still pending, `abortedResult` is the original execution promise so callers can await the eventual partial response separately from the abort rejection.

For example, if a query aborts while a field resolver is pending, `execute()` rejects with the original abort reason as the error message and cause, while `error.abortedResult` later resolves to a response such as `{ data: { blocker: null }, errors: [{ message: 'Aborted!', path: ['blocker'] }] }`.

Incremental execution follows the same pattern for the pending initial result, but aborts after the initial incremental payload continue to surface as rejection of `next` calls on the `subsequentResults` iterator (without this new wrapper).
@yaacovCR yaacovCR force-pushed the expose-partial-result-on-abort branch from 9eda645 to 38744c4 Compare April 22, 2026 10:53
@yaacovCR yaacovCR merged commit 300ccfc into graphql:17.x.x Apr 22, 2026
21 of 22 checks passed
@yaacovCR yaacovCR deleted the expose-partial-result-on-abort branch April 22, 2026 10:58
@phryneas
Copy link
Copy Markdown
Contributor

Just saw this, sorry. I honestly don't remember anymore :)

yaacovCR added a commit that referenced this pull request May 7, 2026
## v17.0.0-beta.0 (2026-05-07)

#### Breaking Change 💥
* [#4634](#4634) Use Object.create(null) over {} to avoid prototype issues ([@benjie](https://github.com/benjie))
* [#4700](#4700) Demote createSourceEventStream to helper taking ValidatedExecutionArgs ([@yaacovCR](https://github.com/yaacovCR))
* [#4703](#4703) rename executeQueryOrMutationOrSubscriptionEvent to executeRootSelectionSet ([@yaacovCR](https://github.com/yaacovCR))
* [#4708](#4708) fix(variables): treat undefined as absent with respect to variables ([@yaacovCR](https://github.com/yaacovCR))
* [#4710](#4710) fix: ignore undefined-valued unknown fields in input objects ([@yaacovCR](https://github.com/yaacovCR))

#### New Feature 🚀
* [#4658](#4658) feat(execution): expose asyncWorkFinished execution hook ([@yaacovCR](https://github.com/yaacovCR))
* [#4674](#4674) feat(execution): expose partial result on abort errors ([@yaacovCR](https://github.com/yaacovCR))
* [#4701](#4701) export validateExecutionArgs helper ([@yaacovCR](https://github.com/yaacovCR))
* [#4702](#4702) feat: add ValidatedSubscriptionArgs and validateSubscriptionArgs ([@yaacovCR](https://github.com/yaacovCR))

#### Bug Fix 🐞
* [#4637](#4637) refactor(queue): replace stopped promise with onStop handlers ([@yaacovCR](https://github.com/yaacovCR))
* [#4641](#4641) refactor(incremental): close stream iterator only on abnormal stop ([@yaacovCR](https://github.com/yaacovCR))
* [#4642](#4642) fix(incremental): await async incremental cleanup ([@yaacovCR](https://github.com/yaacovCR))
* [#4644](#4644) fix(withConcurrentAbruptClose): do not close unnecessarily ([@yaacovCR](https://github.com/yaacovCR))
* [#4645](#4645) fix(cancellablePromise): handle rejection when cancelling already-aborted promise ([@yaacovCR](https://github.com/yaacovCR))
* [#4643](#4643) fix(execute): handle list promise rejections ([@yaacovCR](https://github.com/yaacovCR))
* [#4646](#4646) fix(execute): handle defaultTypeResolver promise rejections ([@yaacovCR](https://github.com/yaacovCR))
* [#4648](#4648) refactor(executor): separate finish/abort orchestration ([@yaacovCR](https://github.com/yaacovCR))
* [#4655](#4655) fix(execution): finish executors before publishing responses ([@yaacovCR](https://github.com/yaacovCR))
* [#4661](#4661) fix: bubbling sync errors need not become async ([@yaacovCR](https://github.com/yaacovCR))
* [#4663](#4663) fix(executor): let aborted async paths reach finish ([@yaacovCR](https://github.com/yaacovCR))
* [#4657](#4657) refactor(execution): track pending work ([@yaacovCR](https://github.com/yaacovCR))
* [#4664](#4664) fix(perf): hoist error creation ([@yaacovCR](https://github.com/yaacovCR))
* [#4665](#4665) execution: reduce parent executor retention in incremental callbacks ([@yaacovCR](https://github.com/yaacovCR))
* [#4671](#4671) fix(valueFromAST): forward port #4652 ([@yaacovCR](https://github.com/yaacovCR))
* [#4672](#4672) fix(execution): convert all promise-like results to promises ([@yaacovCR](https://github.com/yaacovCR))
* [#4518](#4518) Fix TypeInfo.getInputType() for custom scalar list literals. ([@yuchenshi](https://github.com/yuchenshi))
* [#4692](#4692) fix incremental label null validation ([@jbellenger](https://github.com/jbellenger))
* [#4711](#4711) fix(coerceInputLiteral): null variable input object fields should override defaults ([@yaacovCR](https://github.com/yaacovCR))
* [#4712](#4712) fix: name fragment variables as such in execution errors ([@yaacovCR](https://github.com/yaacovCR))
* [#4714](#4714) fix: enhance runtime invalid default value error messages ([@yaacovCR](https://github.com/yaacovCR))
* [#4715](#4715) unify OneOf non-null and count error messages ([@yaacovCR](https://github.com/yaacovCR))
* [#4716](#4716) fix(OneOf): fail coercion when two fields are provided "pre-coercion-only" ([@yaacovCR](https://github.com/yaacovCR))
* [#4717](#4717) fix(valueFromAST): reject unknown input object fields ([@yaacovCR](https://github.com/yaacovCR))
* [#4718](#4718) fix(OneOf): count only known fields for one-of validation ([@yaacovCR](https://github.com/yaacovCR))
* [#4719](#4719) fix(OneOf): fail coercion for OneOf is the single field is invalidly provided by a default ([@yaacovCR](https://github.com/yaacovCR))

#### Polish 💅
<details>
<summary> 19 PRs were merged </summary>

* [#4635](#4635) polish: remove a few superfluous awaits ([@yaacovCR](https://github.com/yaacovCR))
* [#4636](#4636) allow forwarding of cancellation reasons within computations/queues ([@yaacovCR](https://github.com/yaacovCR))
* [#4638](#4638) test(queue): cover async onStop cleanup ([@yaacovCR](https://github.com/yaacovCR))
* [#4639](#4639) refactor(computation): rename cancel to abort ([@yaacovCR](https://github.com/yaacovCR))
* [#4640](#4640) refactor(workqueue): propagate async computation abort cleanup ([@yaacovCR](https://github.com/yaacovCR))
* [#4647](#4647) refactor(executor): rename finished state to aborted ([@yaacovCR](https://github.com/yaacovCR))
* [#4649](#4649) refactor(cancellablePromise): extract withCancellation ([@yaacovCR](https://github.com/yaacovCR))
* [#4650](#4650) refactor(executor): replace internal abort signal with lightweight version ([@yaacovCR](https://github.com/yaacovCR))
* [#4656](#4656) refactor(execution): introduce shared execution context ([@yaacovCR](https://github.com/yaacovCR))
* [#4653](#4653) fix: avoid prototype-colliding names in execution values ([@abishekgiri](https://github.com/abishekgiri))
* [#4673](#4673) refactor(execution): separate response construction from finish checks ([@yaacovCR](https://github.com/yaacovCR))
* [#4675](#4675) polish(execution): remove obsolete abort async checks ([@yaacovCR](https://github.com/yaacovCR))
* [#4686](#4686) refactor(benchmark): extract shared mean stats helper ([@yaacovCR](https://github.com/yaacovCR))
* [#4687](#4687) polish(benchmark): handle theoretical stats errors ([@yaacovCR](https://github.com/yaacovCR))
* [#4693](#4693) polish: add expectToThrow test helper ([@yaacovCR](https://github.com/yaacovCR))
* [#4694](#4694) polish: add expectPromise.toReject helper ([@yaacovCR](https://github.com/yaacovCR))
* [#4695](#4695) polish: clean up stream tests ([@yaacovCR](https://github.com/yaacovCR))
* [#4696](#4696) polish: introduce spy test helper ([@yaacovCR](https://github.com/yaacovCR))
* [#4704](#4704) refactor: add executionMode/serially argument to executeRootSelectionSet ([@yaacovCR](https://github.com/yaacovCR))
</details>

#### Internal 🏠
<details>
<summary> 17 PRs were merged </summary>

* [#4676](#4676) chore(deps): upgrade to ts v6 ([@yaacovCR](https://github.com/yaacovCR))
* [#4677](#4677) internal: refactor benchmark runner structure ([@yaacovCR](https://github.com/yaacovCR))
* [#4678](#4678) internal: run benchmarks through worker files ([@yaacovCR](https://github.com/yaacovCR))
* [#4679](#4679) internal: read benchmark names through dedicated worker ([@yaacovCR](https://github.com/yaacovCR))
* [#4680](#4680) internal: separate benchmark timing and memory sampling ([@yaacovCR](https://github.com/yaacovCR))
* [#4681](#4681) fix(benchmark): use node flags only for memory sampling ([@yaacovCR](https://github.com/yaacovCR))
* [#4682](#4682) internal: use mitata for benchmark timing ([@yaacovCR](https://github.com/yaacovCR))
* [#4683](#4683) refactor(benchmark): integrate run + sampling files ([@yaacovCR](https://github.com/yaacovCR))
* [#4684](#4684) benchmark: perform timing tests in rounds to reduce variance ([@yaacovCR](https://github.com/yaacovCR))
* [#4685](#4685) benchmark: stop timing after stable pairwise comparisons ([@yaacovCR](https://github.com/yaacovCR))
* [#4688](#4688) benchmark: cache revision archives ([@yaacovCR](https://github.com/yaacovCR))
* [#4689](#4689) benchmark: report paired benchmark comparisons ([@yaacovCR](https://github.com/yaacovCR))
* [#4690](#4690) benchmark: add async object field benchmark ([@yaacovCR](https://github.com/yaacovCR))
* [#4697](#4697) internal: add runtime option to benchmark ([@yaacovCR](https://github.com/yaacovCR))
* [#4713](#4713) benchmark: add benchmark for schema validation ([@yaacovCR](https://github.com/yaacovCR))
* [#4722](#4722) internal: npm release:prepare should set publishTag ([@yaacovCR](https://github.com/yaacovCR))
* [#4723](#4723) fix(changelog): ignore open associated PRs ([@yaacovCR](https://github.com/yaacovCR))
</details>

#### Committers: 5
* Abishek Kumar Giri([@abishekgiri](https://github.com/abishekgiri))
* Benjie([@benjie](https://github.com/benjie))
* James Bellenger([@jbellenger](https://github.com/jbellenger))
* Yaacov Rydzinski ([@yaacovCR](https://github.com/yaacovCR))
* Yuchen Shi([@yuchenshi](https://github.com/yuchenshi))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

PR: feature 🚀 requires increase of "minor" version number

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants