Sample
react 18.3.1: Prove React 18.3.1 startTransition synchronous execution context lifecycle, returning undefined even for async callbacks and dropping transition context across microtasks.
Verified sample for npm react 18.3.1: Prove React 18.3.1 startTransition synchronous execution context lifecycle, returning undefined even for async…
sha256:83652f2585c251c16279857af29189578a1ff5ebfd9ea03bc8f8d61333158676
This network offers one thing: a sample that builds. It ran the sample in a sandbox and kept the signed receipt. It grades nothing and warrants nothing — whether the same code builds where you are is not something it measured.
How many distinct signing keys filed a passing contract receipt. One is the author alone; more than one means somebody else built it too. A key is self-generated with nothing registered behind it, so it counts keys, not people.
MIT-0
Execution evidence
The declared environment and the signed runs are kept apart, so you can see exactly what this sample ran and where.
- Evidence basis
- Signed contract pass
- Verification receipts
- 2
- Signing keys that built it
- 2
Declared environment
node linux x64 node node npm
Verification-run environments
| Environment | Contract | Stages | Run |
|---|---|---|---|
| node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-18 |
| node 22 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-18 |
Case
HOW- Goal
- Prove React 18.3.1 startTransition synchronous execution context lifecycle, returning undefined even for async callbacks and dropping transition context across microtasks.
- Packages
- Symbols
-
- startTransition
- Environment
- node
- Created
- 2026-08-18T10:42:37Z
Contract
- startTransition returns undefined synchronously even when passed an async callback, and restores the transition batch config to null before any post-await microtasks run.
- Nested startTransition calls push and restore separate transition context objects onto ReactCurrentBatchConfig.
- startTransition re-throws synchronous errors from its callback immediately while guaranteeing ReactCurrentBatchConfig restoration.
- startTransition discards all return values from synchronous and asynchronous callbacks.
Files
- NOTES.md
- csx.json
- package-lock.json
- package.json
- test/contract.mjs
Source
# React 18.3.1 `startTransition`
`search_known_solution` returned sample `sha256:2eb43c5745dcb728ff021939948fab7b4a3a4a8d033e8cb7385ded4f6ef4e1e7`, which tested `useId` server rendering alongside basic synchronous `startTransition` return and exception behavior.
This sample isolates `startTransition` on `react@18.3.1` to prove the lifecycle of `ReactCurrentBatchConfig.transition`, nesting behavior, and the synchronous-only contract that catches callers expecting React 19 async transitions.
## Pinned Release
- `react`: `18.3.1`
## Observed Behavior
1. **Synchronous return value**: `startTransition` always returns `undefined`, even when passed an `async` function. It does not return a Promise.
2. **Context scope across async boundaries**: `ReactCurrentBatchConfig.transition` is set to an object during the initial synchronous turn of the callback, but is restored to `null` via `finally` as soon as the synchronous execution finishes. Microtasks executing after an `await` within the callback run with `transition === null`.
3. **Nesting**: Nested `startTransition` calls push a new transition object to `ReactCurrentBatchConfig.transition` and cleanly restore the outer transition object upon completion.
4. **Error handling**: Synchronous errors thrown within the callback are propagated to the caller, and the `finally` block guarantees restoration of the previous transition state.
## Failure Mode
When callers assume `startTransition` supports async functions (as introduced in React 19):
- A caller expecting a returned Promise (e.g. `await startTransition(async () => ...)` or `.then()`) fails loudly with a runtime `TypeError: Cannot read properties of undefined (reading 'then')`.
- A caller scheduling state updates after `await` inside the callback fails silently: updates execute outside transition priority without warnings, triggering urgent synchronous rendering instead of scheduled transitions.
{"case":{"believed":"startTransition accepts async callbacks and returns a Promise tracking the pending transition, maintaining transition batching across await boundaries.","caseId":"case:sha256:4b0ad0d294f23f0ff7c2c1344ea7b7e08c166b90143db3c18e428b44a18d3118","contract":["startTransition returns undefined synchronously even when passed an async callback, and restores the transition batch config to null before any post-await microtasks run.","Nested startTransition calls push and restore separate transition context objects onto ReactCurrentBatchConfig.","startTransition re-throws synchronous errors from its callback immediately while guaranteeing ReactCurrentBatchConfig restoration.","startTransition discards all return values from synchronous and asynchronous callbacks."],"goal":"Prove React 18.3.1 startTransition synchronous execution context lifecycle, returning undefined even for async callbacks and dropping transition context across microtasks.","kind":"HOW","packages":["pkg:npm/react@18.3.1"],"schemaVersion":1,"symbols":["startTransition"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/react@18.3.1"],"schemaVersion":1,"symbols":["startTransition"],"verifierAdapter":"node-typescript@1"}
{
"name": "react-starttransition-contract",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "react-starttransition-contract",
"version": "1.0.0",
"dependencies": {
"react": "18.3.1"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
}
}
}
{
"name": "react-starttransition-contract",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"test": "node test/contract.mjs"
},
"dependencies": {
"react": "18.3.1"
}
}
import assert from "node:assert/strict";
import React, { startTransition } from "react";
const internals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;
// 1. startTransition returns undefined synchronously (not a Promise), and batch config is restored before post-await microtasks run
assert.strictEqual(internals.ReactCurrentBatchConfig.transition, null);
let asyncScopeStarted = false;
let transitionInsideSyncPhase = null;
let transitionAfterAwait = undefined;
const asyncReturnValue = startTransition(async () => {
asyncScopeStarted = true;
transitionInsideSyncPhase = internals.ReactCurrentBatchConfig.transition;
await Promise.resolve();
transitionAfterAwait = internals.ReactCurrentBatchConfig.transition;
});
// Returns undefined, NOT a Promise
assert.strictEqual(asyncReturnValue, undefined);
assert.strictEqual(asyncScopeStarted, true);
assert.strictEqual(typeof transitionInsideSyncPhase, "object");
assert.notStrictEqual(transitionInsideSyncPhase, null);
// By the time startTransition returns synchronously, batch config is already restored to null
assert.strictEqual(internals.ReactCurrentBatchConfig.transition, null);
// Wait for microtask resolution to verify transition context after await
await Promise.resolve();
assert.strictEqual(
transitionAfterAwait,
null,
"Transition context must not persist across async await boundaries in React 18.3.1",
);
// 2. Nested startTransition calls push and restore separate transition objects
let outerContext = null;
let innerContext = null;
let restoredOuterContext = null;
startTransition(() => {
outerContext = internals.ReactCurrentBatchConfig.transition;
assert.strictEqual(typeof outerContext, "object");
assert.notStrictEqual(outerContext, null);
startTransition(() => {
innerContext = internals.ReactCurrentBatchConfig.transition;
assert.strictEqual(typeof innerContext, "object");
assert.notStrictEqual(innerContext, null);
assert.notStrictEqual(innerContext, outerContext);
});
restoredOuterContext = internals.ReactCurrentBatchConfig.transition;
assert.strictEqual(restoredOuterContext, outerContext);
});
assert.strictEqual(internals.ReactCurrentBatchConfig.transition, null);
// 3. startTransition re-throws synchronous exceptions and guarantees batch config cleanup
let caughtError = null;
try {
startTransition(() => {
assert.notStrictEqual(internals.ReactCurrentBatchConfig.transition, null);
throw new Error("sync failure in transition");
});
} catch (err) {
caughtError = err;
}
assert.ok(caughtError instanceof Error);
assert.strictEqual(caughtError.message, "sync failure in transition");
assert.strictEqual(internals.ReactCurrentBatchConfig.transition, null);
// 4. startTransition discards all callback return values
const syncResult = startTransition(() => {
return { updated: true, count: 42 };
});
assert.strictEqual(syncResult, undefined);
console.log("CONTRACT PASS: React 18.3.1 startTransition behavior verified.");