Skip to content

[Flight & DevTools] Strip the whole "async " prefix from V8 stack frame names - #37608

Merged
hoxyq merged 2 commits into
react:mainfrom
Dev-next-gen:fix/devtools-async-stack-frame-name
Sep 13, 2026
Merged

hoxyq merged 2 commits into
react:mainfrom
Dev-next-gen:fix/devtools-async-stack-frame-name

Conversation

@Dev-next-gen

@Dev-next-gen Dev-next-gen commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

V8 prefixes async call sites with async when it prints a stack, like this:

Error: boom
    at inner (/tmp/asy.js:1:44)
    at async outerName (/tmp/asy.js:2:30)

parseStackTraceFromChromeStack captures async outerName as the frame name and strips the prefix here:

} else if (name.startsWith('async ')) {
  name = name.slice(5);
  isAsync = true;
}

'async ' is six characters, so slice(5) leaves the space behind and the frame name comes back as ' outerName' rather than 'outerName'.

I noticed it while reading the parser, and it is not purely cosmetic: that name is the first element of the ReactFunctionLocation returned by extractLocationFromComponentStack and extractLocationFromOwnerStack, which backend/fiber/renderer.js stores as instance.source. Any async component whose frame reaches that path is recorded under a name with a stray leading space.

The same line exists in packages/react-server/src/ReactFlightStackConfigV8.js, which the DevTools file is a copy of. After review I fixed it in this PR as well, in a second commit. There it only matters on the fallback path that parses an already formatted stack string, when the error's stack was read or assigned before React reaches it. #37130 is open on that file too, but it does not touch these lines.

How did you test this change?

I first confirmed the format V8 actually emits, rather than assuming it:

$ node -e 'async function inner(){await null;throw new Error("boom")}
async function outerName(){await inner()}
outerName().catch(e=>console.log(e.stack))'
Error: boom
    at inner ([eval]:1:52)
    at async outerName ([eval]:2:33)

Then I added a case to the existing extractLocationFromComponentStack block in utils-test.js. Against main it fails with exactly the leading space:

  ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name

    - Expected  - 1
    + Received  + 1

      Array [
    -   "Comments",
    +   " Comments",
        "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js",
        5,
        9236,
      ]

With the one-character fix applied:

$ yarn test --build --project=devtools -r=experimental utils-test
PASS packages/react-devtools-shared/src/__tests__/utils-test.js
Tests:       63 passed, 63 total

I also ran the whole DevTools project before and after to check I was not moving anything else. Both runs end at 9 failed, 4 failed suites, the same test names each time (componentStacks, console, inspectedElement, legacy/inspectElement), so those failures are pre-existing on main in my environment and unrelated to this change. The only difference between the two runs is my new test: 586 passed before, 587 after.

For the Flight side I added ReactFlightStackConfigV8-test.js, which assigns a formatted stack to an error and checks what parseStackTrace returns. Against main it fails with the same leading space (" outerName"); with the fix it passes on stable and experimental in development. It is gated to __DEV__ because that fallback goes through the DEV-only stack cache. ReactFlightServer-test and ReactFlightAsyncDebugInfo-test still pass next to it (23 tests).

prettier and eslint are clean on all changed files. yarn flow dom-node reported no errors for the DevTools commit; I did not rerun Flow after the one-character Flight change.

AI tools used

V8 prefixes async call sites with "async ", as in
`at async Comments (app.js:5:9236)`. The chrome stack parser detects
that prefix with `startsWith('async ')` but then cuts it with
`slice(5)`, which is one character short of the six in `'async '`. The
leading space survives, so the frame name comes out as `" Comments"`
instead of `"Comments"`.

That name is what `extractLocationFromComponentStack` and
`extractLocationFromOwnerStack` put in the `ReactFunctionLocation` they
hand back to the DevTools fiber renderer, so any async component ends
up recorded under a name with a stray leading space.

Added a case to the existing `extractLocationFromComponentStack`
tests that fails before the change and passes after.
@meta-cla meta-cla Bot added the CLA Signed label Sep 11, 2026
@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

Warning

These sizes are for 3493725, which is no longer the head of this pull request.

A build for 1d7a34f is in progress.

No size report: this pull request only touches packages/react-devtools, which does not affect production bundle size.

Generated by sizebot against 3493725

@hoxyq hoxyq left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

Could you please also fix https://github.com/react/react/blob/main/packages/react-server/src/ReactFlightStackConfigV8.js#L239-L242 in this PR, or open a separate one?

ReactFlightStackConfigV8 has the same off-by-one as the DevTools copy:
when it falls back to parsing a formatted stack, it detects the prefix
with `startsWith('async ')` and then cuts it with `slice(5)`, so an
async frame comes back named `" outerName"`.

That fallback runs when the error's stack was already read or assigned
before React gets to it. Added a test that assigns a formatted stack and
checks the parsed frames; it fails before the change and passes after.
@Dev-next-gen

Copy link
Copy Markdown
Contributor Author

@hoxyq Done in this PR: 1d7a34f changes the same slice(5) to slice(6) in ReactFlightStackConfigV8.js.

In that file the string parser only runs as a fallback, when the error's stack has already been read or assigned before React gets to it, so I added a small ReactFlightStackConfigV8-test.js that assigns a formatted stack and checks what parseStackTrace returns. It fails on the current code with " outerName" and passes with the change. ReactFlightServer-test and ReactFlightAsyncDebugInfo-test still pass as well.

@hoxyq hoxyq changed the title [DevTools] Strip the whole "async " prefix from V8 stack frame names [Flight & DevTools] Strip the whole "async " prefix from V8 stack frame names Sep 13, 2026
@hoxyq
hoxyq merged commit ccea5fd into react:main Sep 13, 2026
239 checks passed
github-actions Bot pushed a commit that referenced this pull request Sep 13, 2026
…me names (#37608)

## Summary

V8 prefixes async call sites with `async ` when it prints a stack, like
this:

```
Error: boom
    at inner (/tmp/asy.js:1:44)
    at async outerName (/tmp/asy.js:2:30)
```

`parseStackTraceFromChromeStack` captures `async outerName` as the frame
name and strips the prefix here:

```js
} else if (name.startsWith('async ')) {
  name = name.slice(5);
  isAsync = true;
}
```

`'async '` is six characters, so `slice(5)` leaves the space behind and
the frame name comes back as `' outerName'` rather than `'outerName'`.

I noticed it while reading the parser, and it is not purely cosmetic:
that name is the first element of the `ReactFunctionLocation` returned
by `extractLocationFromComponentStack` and
`extractLocationFromOwnerStack`, which `backend/fiber/renderer.js`
stores as `instance.source`. Any async component whose frame reaches
that path is recorded under a name with a stray leading space.

The same line exists in
`packages/react-server/src/ReactFlightStackConfigV8.js`, which the
DevTools file is a copy of. After review I fixed it in this PR as well,
in a second commit. There it only matters on the fallback path that
parses an already formatted stack string, when the error's `stack` was
read or assigned before React reaches it. #37130 is open on that file
too, but it does not touch these lines.

## How did you test this change?

I first confirmed the format V8 actually emits, rather than assuming it:

```
$ node -e 'async function inner(){await null;throw new Error("boom")}
async function outerName(){await inner()}
outerName().catch(e=>console.log(e.stack))'
Error: boom
    at inner ([eval]:1:52)
    at async outerName ([eval]:2:33)
```

Then I added a case to the existing `extractLocationFromComponentStack`
block in `utils-test.js`. Against `main` it fails with exactly the
leading space:

```
  ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name

    - Expected  - 1
    + Received  + 1

      Array [
    -   "Comments",
    +   " Comments",
        "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js",
        5,
        9236,
      ]
```

With the one-character fix applied:

```
$ yarn test --build --project=devtools -r=experimental utils-test
PASS packages/react-devtools-shared/src/__tests__/utils-test.js
Tests:       63 passed, 63 total
```

I also ran the whole DevTools project before and after to check I was
not moving anything else. Both runs end at `9 failed, 4 failed suites`,
the same test names each time (`componentStacks`, `console`,
`inspectedElement`, `legacy/inspectElement`), so those failures are
pre-existing on `main` in my environment and unrelated to this change.
The only difference between the two runs is my new test: 586 passed
before, 587 after.

For the Flight side I added `ReactFlightStackConfigV8-test.js`, which
assigns a formatted stack to an error and checks what `parseStackTrace`
returns. Against `main` it fails with the same leading space (`"
outerName"`); with the fix it passes on stable and experimental in
development. It is gated to `__DEV__` because that fallback goes through
the DEV-only stack cache. `ReactFlightServer-test` and
`ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests).

`prettier` and `eslint` are clean on all changed files. `yarn flow
dom-node` reported no errors for the DevTools commit; I did not rerun
Flow after the one-character Flight change.

AI tools used

DiffTrain build for [ccea5fd](ccea5fd)
github-actions Bot pushed a commit to code/lib-react that referenced this pull request Sep 13, 2026
…me names (react#37608)

## Summary

V8 prefixes async call sites with `async ` when it prints a stack, like
this:

```
Error: boom
    at inner (/tmp/asy.js:1:44)
    at async outerName (/tmp/asy.js:2:30)
```

`parseStackTraceFromChromeStack` captures `async outerName` as the frame
name and strips the prefix here:

```js
} else if (name.startsWith('async ')) {
  name = name.slice(5);
  isAsync = true;
}
```

`'async '` is six characters, so `slice(5)` leaves the space behind and
the frame name comes back as `' outerName'` rather than `'outerName'`.

I noticed it while reading the parser, and it is not purely cosmetic:
that name is the first element of the `ReactFunctionLocation` returned
by `extractLocationFromComponentStack` and
`extractLocationFromOwnerStack`, which `backend/fiber/renderer.js`
stores as `instance.source`. Any async component whose frame reaches
that path is recorded under a name with a stray leading space.

The same line exists in
`packages/react-server/src/ReactFlightStackConfigV8.js`, which the
DevTools file is a copy of. After review I fixed it in this PR as well,
in a second commit. There it only matters on the fallback path that
parses an already formatted stack string, when the error's `stack` was
read or assigned before React reaches it. react#37130 is open on that file
too, but it does not touch these lines.

## How did you test this change?

I first confirmed the format V8 actually emits, rather than assuming it:

```
$ node -e 'async function inner(){await null;throw new Error("boom")}
async function outerName(){await inner()}
outerName().catch(e=>console.log(e.stack))'
Error: boom
    at inner ([eval]:1:52)
    at async outerName ([eval]:2:33)
```

Then I added a case to the existing `extractLocationFromComponentStack`
block in `utils-test.js`. Against `main` it fails with exactly the
leading space:

```
  ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name

    - Expected  - 1
    + Received  + 1

      Array [
    -   "Comments",
    +   " Comments",
        "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js",
        5,
        9236,
      ]
```

With the one-character fix applied:

```
$ yarn test --build --project=devtools -r=experimental utils-test
PASS packages/react-devtools-shared/src/__tests__/utils-test.js
Tests:       63 passed, 63 total
```

I also ran the whole DevTools project before and after to check I was
not moving anything else. Both runs end at `9 failed, 4 failed suites`,
the same test names each time (`componentStacks`, `console`,
`inspectedElement`, `legacy/inspectElement`), so those failures are
pre-existing on `main` in my environment and unrelated to this change.
The only difference between the two runs is my new test: 586 passed
before, 587 after.

For the Flight side I added `ReactFlightStackConfigV8-test.js`, which
assigns a formatted stack to an error and checks what `parseStackTrace`
returns. Against `main` it fails with the same leading space (`"
outerName"`); with the fix it passes on stable and experimental in
development. It is gated to `__DEV__` because that fallback goes through
the DEV-only stack cache. `ReactFlightServer-test` and
`ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests).

`prettier` and `eslint` are clean on all changed files. `yarn flow
dom-node` reported no errors for the DevTools commit; I did not rerun
Flow after the one-character Flight change.

AI tools used

DiffTrain build for [ccea5fd](react@ccea5fd)
github-actions Bot pushed a commit to code/lib-react that referenced this pull request Sep 13, 2026
…me names (react#37608)

## Summary

V8 prefixes async call sites with `async ` when it prints a stack, like
this:

```
Error: boom
    at inner (/tmp/asy.js:1:44)
    at async outerName (/tmp/asy.js:2:30)
```

`parseStackTraceFromChromeStack` captures `async outerName` as the frame
name and strips the prefix here:

```js
} else if (name.startsWith('async ')) {
  name = name.slice(5);
  isAsync = true;
}
```

`'async '` is six characters, so `slice(5)` leaves the space behind and
the frame name comes back as `' outerName'` rather than `'outerName'`.

I noticed it while reading the parser, and it is not purely cosmetic:
that name is the first element of the `ReactFunctionLocation` returned
by `extractLocationFromComponentStack` and
`extractLocationFromOwnerStack`, which `backend/fiber/renderer.js`
stores as `instance.source`. Any async component whose frame reaches
that path is recorded under a name with a stray leading space.

The same line exists in
`packages/react-server/src/ReactFlightStackConfigV8.js`, which the
DevTools file is a copy of. After review I fixed it in this PR as well,
in a second commit. There it only matters on the fallback path that
parses an already formatted stack string, when the error's `stack` was
read or assigned before React reaches it. react#37130 is open on that file
too, but it does not touch these lines.

## How did you test this change?

I first confirmed the format V8 actually emits, rather than assuming it:

```
$ node -e 'async function inner(){await null;throw new Error("boom")}
async function outerName(){await inner()}
outerName().catch(e=>console.log(e.stack))'
Error: boom
    at inner ([eval]:1:52)
    at async outerName ([eval]:2:33)
```

Then I added a case to the existing `extractLocationFromComponentStack`
block in `utils-test.js`. Against `main` it fails with exactly the
leading space:

```
  ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name

    - Expected  - 1
    + Received  + 1

      Array [
    -   "Comments",
    +   " Comments",
        "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js",
        5,
        9236,
      ]
```

With the one-character fix applied:

```
$ yarn test --build --project=devtools -r=experimental utils-test
PASS packages/react-devtools-shared/src/__tests__/utils-test.js
Tests:       63 passed, 63 total
```

I also ran the whole DevTools project before and after to check I was
not moving anything else. Both runs end at `9 failed, 4 failed suites`,
the same test names each time (`componentStacks`, `console`,
`inspectedElement`, `legacy/inspectElement`), so those failures are
pre-existing on `main` in my environment and unrelated to this change.
The only difference between the two runs is my new test: 586 passed
before, 587 after.

For the Flight side I added `ReactFlightStackConfigV8-test.js`, which
assigns a formatted stack to an error and checks what `parseStackTrace`
returns. Against `main` it fails with the same leading space (`"
outerName"`); with the fix it passes on stable and experimental in
development. It is gated to `__DEV__` because that fallback goes through
the DEV-only stack cache. `ReactFlightServer-test` and
`ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests).

`prettier` and `eslint` are clean on all changed files. `yarn flow
dom-node` reported no errors for the DevTools commit; I did not rerun
Flow after the one-character Flight change.

AI tools used

DiffTrain build for [ccea5fd](react@ccea5fd)
github-actions Bot pushed a commit to seshan18/react that referenced this pull request Sep 13, 2026
…me names (react#37608)

## Summary

V8 prefixes async call sites with `async ` when it prints a stack, like
this:

```
Error: boom
    at inner (/tmp/asy.js:1:44)
    at async outerName (/tmp/asy.js:2:30)
```

`parseStackTraceFromChromeStack` captures `async outerName` as the frame
name and strips the prefix here:

```js
} else if (name.startsWith('async ')) {
  name = name.slice(5);
  isAsync = true;
}
```

`'async '` is six characters, so `slice(5)` leaves the space behind and
the frame name comes back as `' outerName'` rather than `'outerName'`.

I noticed it while reading the parser, and it is not purely cosmetic:
that name is the first element of the `ReactFunctionLocation` returned
by `extractLocationFromComponentStack` and
`extractLocationFromOwnerStack`, which `backend/fiber/renderer.js`
stores as `instance.source`. Any async component whose frame reaches
that path is recorded under a name with a stray leading space.

The same line exists in
`packages/react-server/src/ReactFlightStackConfigV8.js`, which the
DevTools file is a copy of. After review I fixed it in this PR as well,
in a second commit. There it only matters on the fallback path that
parses an already formatted stack string, when the error's `stack` was
read or assigned before React reaches it. react#37130 is open on that file
too, but it does not touch these lines.

## How did you test this change?

I first confirmed the format V8 actually emits, rather than assuming it:

```
$ node -e 'async function inner(){await null;throw new Error("boom")}
async function outerName(){await inner()}
outerName().catch(e=>console.log(e.stack))'
Error: boom
    at inner ([eval]:1:52)
    at async outerName ([eval]:2:33)
```

Then I added a case to the existing `extractLocationFromComponentStack`
block in `utils-test.js`. Against `main` it fails with exactly the
leading space:

```
  ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name

    - Expected  - 1
    + Received  + 1

      Array [
    -   "Comments",
    +   " Comments",
        "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js",
        5,
        9236,
      ]
```

With the one-character fix applied:

```
$ yarn test --build --project=devtools -r=experimental utils-test
PASS packages/react-devtools-shared/src/__tests__/utils-test.js
Tests:       63 passed, 63 total
```

I also ran the whole DevTools project before and after to check I was
not moving anything else. Both runs end at `9 failed, 4 failed suites`,
the same test names each time (`componentStacks`, `console`,
`inspectedElement`, `legacy/inspectElement`), so those failures are
pre-existing on `main` in my environment and unrelated to this change.
The only difference between the two runs is my new test: 586 passed
before, 587 after.

For the Flight side I added `ReactFlightStackConfigV8-test.js`, which
assigns a formatted stack to an error and checks what `parseStackTrace`
returns. Against `main` it fails with the same leading space (`"
outerName"`); with the fix it passes on stable and experimental in
development. It is gated to `__DEV__` because that fallback goes through
the DEV-only stack cache. `ReactFlightServer-test` and
`ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests).

`prettier` and `eslint` are clean on all changed files. `yarn flow
dom-node` reported no errors for the DevTools commit; I did not rerun
Flow after the one-character Flight change.

AI tools used

DiffTrain build for [ccea5fd](react@ccea5fd)
github-actions Bot pushed a commit to seshan18/react that referenced this pull request Sep 13, 2026
…me names (react#37608)

## Summary

V8 prefixes async call sites with `async ` when it prints a stack, like
this:

```
Error: boom
    at inner (/tmp/asy.js:1:44)
    at async outerName (/tmp/asy.js:2:30)
```

`parseStackTraceFromChromeStack` captures `async outerName` as the frame
name and strips the prefix here:

```js
} else if (name.startsWith('async ')) {
  name = name.slice(5);
  isAsync = true;
}
```

`'async '` is six characters, so `slice(5)` leaves the space behind and
the frame name comes back as `' outerName'` rather than `'outerName'`.

I noticed it while reading the parser, and it is not purely cosmetic:
that name is the first element of the `ReactFunctionLocation` returned
by `extractLocationFromComponentStack` and
`extractLocationFromOwnerStack`, which `backend/fiber/renderer.js`
stores as `instance.source`. Any async component whose frame reaches
that path is recorded under a name with a stray leading space.

The same line exists in
`packages/react-server/src/ReactFlightStackConfigV8.js`, which the
DevTools file is a copy of. After review I fixed it in this PR as well,
in a second commit. There it only matters on the fallback path that
parses an already formatted stack string, when the error's `stack` was
read or assigned before React reaches it. react#37130 is open on that file
too, but it does not touch these lines.

## How did you test this change?

I first confirmed the format V8 actually emits, rather than assuming it:

```
$ node -e 'async function inner(){await null;throw new Error("boom")}
async function outerName(){await inner()}
outerName().catch(e=>console.log(e.stack))'
Error: boom
    at inner ([eval]:1:52)
    at async outerName ([eval]:2:33)
```

Then I added a case to the existing `extractLocationFromComponentStack`
block in `utils-test.js`. Against `main` it fails with exactly the
leading space:

```
  ● utils › extractLocationFromComponentStack › should strip the async prefix from a frame name

    - Expected  - 1
    + Received  + 1

      Array [
    -   "Comments",
    +   " Comments",
        "https://react.dev/_next/static/chunks/848-122f91e9565d9ffa.js",
        5,
        9236,
      ]
```

With the one-character fix applied:

```
$ yarn test --build --project=devtools -r=experimental utils-test
PASS packages/react-devtools-shared/src/__tests__/utils-test.js
Tests:       63 passed, 63 total
```

I also ran the whole DevTools project before and after to check I was
not moving anything else. Both runs end at `9 failed, 4 failed suites`,
the same test names each time (`componentStacks`, `console`,
`inspectedElement`, `legacy/inspectElement`), so those failures are
pre-existing on `main` in my environment and unrelated to this change.
The only difference between the two runs is my new test: 586 passed
before, 587 after.

For the Flight side I added `ReactFlightStackConfigV8-test.js`, which
assigns a formatted stack to an error and checks what `parseStackTrace`
returns. Against `main` it fails with the same leading space (`"
outerName"`); with the fix it passes on stable and experimental in
development. It is gated to `__DEV__` because that fallback goes through
the DEV-only stack cache. `ReactFlightServer-test` and
`ReactFlightAsyncDebugInfo-test` still pass next to it (23 tests).

`prettier` and `eslint` are clean on all changed files. `yarn flow
dom-node` reported no errors for the DevTools commit; I did not rerun
Flow after the one-character Flight change.

AI tools used

DiffTrain build for [ccea5fd](react@ccea5fd)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants