How to fix Cannot use startTransition with async state updates in React

ReactINTERMEDIATEMEDIUMUpdated Dec 9, 2025ยท 133 views

This error occurs in React 18 when trying to use async/await inside startTransition callbacks, causing state updates after await to lose their transition marking. React 18 requires synchronous functions for startTransition, though this limitation is resolved in React 19.

Quick fix

Fixes Cannot use startTransition with async state updates

import { startTransition, useState } from 'react';

function SearchComponent() {
  const [results, setResults] = useState([]);

  const handleSearch = async (query: string) => {
    // Fetch can happen outside startTransition
    const data = await fetchSearchResults(query);

    // Wrap the state update in startTransition
    startTransition(() => {
      setResults(data);
    });
  };

  return (
    <div>
      <input onChange={(e) => handleSearch(e.target.value)} />
      <ResultsList results={results} />
    </div>
  );
}
  1. Wrap post-await state updates in nested startTransition
  2. Use useTransition hook for better control
  3. Move async logic outside the transition callback
  4. Consider upgrading to React 19 for native async support
react โ€” error
import { startTransition, useState } from 'react';
function SearchComponent() {
const [results, setResults] = useState([]);
const handleSearch = async (query: string) => {
// Fetch can happen outside startTransition
const data = await fetchSearchResults(query);
// Wrap the state update in startTransition
startTransition(() => {
setResults(data);
});
};
return (
<div>
<input onChange={(e) => handleSearch(e.target.value)} />
<ResultsList results={results} />
</div>
);
}
Cannot use startTransition with async state updates

What this error means

In React 18, the `startTransition` API requires the callback function to be synchronous. React immediately executes this function and marks all state updates that happen during its synchronous execution as transitions. When you use `await` inside a `startTransition` callback, any state updates that occur after the `await` statement are no longer marked as transitions. This is a JavaScript limitation caused by React losing the scope of the async context. Once execution crosses an `await` boundary, React can no longer track which state updates should be treated as non-urgent transitions versus urgent updates. This means async operations like data fetching cannot be directly wrapped in `startTransition` in React 18. The React team acknowledged this as a known limitation that would be addressed in future versions. In React 19, this restriction has been lifted, and `startTransition` now supports async callbacks natively, allowing developers to perform asynchronous operations without workarounds.

Typical symptoms

  • State updates after await statements are not marked as transitions
  • UI becomes unresponsive during async operations that should be interruptible
  • Console warnings about async functions in startTransition
  • Transitions not working as expected with data fetching logic
  • State updates after promises resolve block the main thread

Common causes

  1. Using async/await directly inside a startTransition callback in React 18
  2. Not wrapping post-await state updates in a nested startTransition call
  3. Expecting React 18 to track async context across await boundaries
  4. Attempting to mark asynchronous data fetching operations as transitions
  5. Missing the synchronous function requirement in React 18 documentation

How to fix "Cannot use startTransition with async state updates"

1Wrap post-await state updates in nested startTransition

In React 18, manually wrap any state updates that occur after await statements in another startTransition call:

tsx
import { startTransition, useState } from 'react';

function SearchComponent() {
  const [results, setResults] = useState([]);

  const handleSearch = async (query: string) => {
    // Fetch can happen outside startTransition
    const data = await fetchSearchResults(query);

    // Wrap the state update in startTransition
    startTransition(() => {
      setResults(data);
    });
  };

  return (
    <div>
      <input onChange={(e) => handleSearch(e.target.value)} />
      <ResultsList results={results} />
    </div>
  );
}

This ensures the state update after the async operation is marked as a transition.

2Use useTransition hook for better control

The useTransition hook provides isPending state and better control over async transitions:

tsx
import { useTransition, useState } from 'react';

function SearchComponent() {
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  const handleSearch = async (query: string) => {
    const data = await fetchSearchResults(query);

    startTransition(() => {
      setResults(data);
    });
  };

  return (
    <div>
      <input onChange={(e) => handleSearch(e.target.value)} />
      {isPending && <Spinner />}
      <ResultsList results={results} />
    </div>
  );
}

The isPending flag helps show loading states during transitions.

3Move async logic outside the transition callback

Keep the async operation outside and only wrap the state update:

tsx
import { startTransition, useState } from 'react';

function DataComponent() {
  const [data, setData] = useState(null);

  const loadData = async () => {
    // Async work happens outside
    const response = await fetch('/api/data');
    const result = await response.json();

    // Only the state update is in startTransition
    startTransition(() => {
      setData(result);
    });
  };

  return (
    <button onClick={loadData}>
      Load Data
    </button>
  );
}

This pattern separates concerns and makes the transition boundary clear.

4Consider upgrading to React 19 for native async support

React 19 removes this limitation entirely by supporting async callbacks in startTransition:

tsx
// React 19 - async functions work directly
import { startTransition, useState } from 'react';

function SearchComponent() {
  const [results, setResults] = useState([]);

  const handleSearch = (query: string) => {
    startTransition(async () => {
      // Async function works directly in React 19
      const data = await fetchSearchResults(query);
      setResults(data);
    });
  };

  return (
    <div>
      <input onChange={(e) => handleSearch(e.target.value)} />
      <ResultsList results={results} />
    </div>
  );
}

If you're starting a new project or can upgrade, React 19 provides a cleaner API.

5Use useDeferredValue for input-driven updates

For search inputs and filters, useDeferredValue can be a simpler alternative:

tsx
import { useDeferredValue, useState, useEffect } from 'react';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  const [results, setResults] = useState([]);

  useEffect(() => {
    async function search() {
      if (deferredQuery) {
        const data = await fetchSearchResults(deferredQuery);
        setResults(data);
      }
    }
    search();
  }, [deferredQuery]);

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <ResultsList results={results} />
    </div>
  );
}

This defers the expensive update without manually managing transitions.

Advanced notes

Why This Limitation Exists

JavaScript's async/await is implemented using promises and microtasks. When execution crosses an await boundary, the function is suspended and the call stack unwinds. React's transition tracking relies on synchronous call stack inspection, which breaks when the function is suspended. This is why React 18 cannot automatically track state updates after await statements.

Future AsyncContext Proposal

The TC39 AsyncContext proposal aims to solve this at the JavaScript language level by providing a way to propagate context across async boundaries. Once AsyncContext is standardized and available, React 18 could theoretically support async transitions without manual wrapping.

Performance Considerations

Transitions are meant for non-urgent updates that can be interrupted. If your async operation is critical (like form submission), don't use startTransition at all - let it be an urgent update. Use transitions only for UI updates that can wait, like filtering large lists or switching tabs.

Concurrent Rendering

startTransition is part of React's concurrent rendering features. It tells React that certain updates are less urgent and can be interrupted if more important work comes in. This improves perceived performance by keeping the UI responsive, but it requires React to know which updates are transitions - hence the synchronous requirement in React 18.

Testing Transition Behavior

When testing components that use startTransition, be aware that transitions may not complete synchronously. Use act() and await for proper test isolation:

tsx
import { act, render, screen } from '@testing-library/react';

test('handles transition updates', async () => {
  render(<SearchComponent />);

  await act(async () => {
    fireEvent.change(screen.getByRole('textbox'), {
      target: { value: 'query' }
    });

    // Wait for async transition to complete
    await new Promise(resolve => setTimeout(resolve, 100));
  });

  expect(screen.getByText('Results')).toBeInTheDocument();
});

Debugging Transitions

Use React DevTools Profiler with "Record why each component rendered" enabled to verify that your state updates are being marked as transitions. Non-transition updates will show as high-priority renders, while transition updates show as low-priority.

Frequently asked questions

What does "Cannot use startTransition with async state updates" mean?
In React 18, the startTransition API requires the callback function to be synchronous. React immediately executes this function and marks all state updates that happen during its synchronous execution as transitions. When you use await inside a startTransition callback, any state updates that occur after the await statement are no longer marked as transitions.
How do I fix "Cannot use startTransition with async state updates"?
This error occurs in React 18 when trying to use async/await inside startTransition callbacks, causing state updates after await to lose their transition marking. React 18 requires synchronous functions for startTransition, though this limitation is resolved in React 19.
What causes "Cannot use startTransition with async state updates"?
Using async/await directly inside a startTransition callback in React 18 Not wrapping post-await state updates in a nested startTransition call Expecting React 18 to track async context across await boundaries
Is the "Cannot use startTransition with async state updates" error serious?
It is a medium-severity error โ€” more of a blocker than a risk. The steps above resolve it.

Official resources & further reading