Skip to content

withIsolationScope failed to capture context for unhandled exception inside setInterval #16460

Closed

Description

@zhiyan114

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/node

SDK Version

9.24.0

Framework Version

Node 22.14.0 LTS

Link to Sentry event

No response

Reproduction Example/SDK Setup

https://github.com/zhiyan114/Firey-Bot/blob/275621a29a3f4181857476fd0a002a83301d91e9/src/services/VoiceChatReward.ts#L77C3-L110C4

In line 81, a user context is attached to the isolation scope, but (unhandled) exception in line 90 didn't capture this information.

The following function is being called here: https://github.com/zhiyan114/Firey-Bot/blob/275621a29a3f4181857476fd0a002a83301d91e9/src/services/VoiceChatReward.ts#L35C4-L35C71

Steps to Reproduce

For minimal reproduction step, the following code should replicate the exact behavior:

  • Use withIsolationScope within setTimeout, setTimeout, or equivalent.
import { init, captureException, consoleLoggingIntegration, extraErrorDataIntegration, flush, logger, rewriteFramesIntegration, withIsolationScope, withIsolationScope } from "@sentry/node";

function caller(bk: ()=> Promise<void>) {
    setTimeout(bk, 1000);
}

process.env["SENTRY_RELEASE"] = execSync(`git -C ${__dirname} rev-parse HEAD`).toString().trim(),
init({
    dsn: "someDSN",
    maxValueLength: 1000,
    tracesSampleRate: 0,
    sendDefaultPii: true,
    _experiments: {
      // Enable logs to be sent to Sentry.
      enableLogs: true,
    },
    integrations: [
        extraErrorDataIntegration({
          depth: 3
        }),
        
        consoleLoggingIntegration({ levels: ['log', 'error', 'warn'] }),
        
        rewriteFramesIntegration({
          iteratee(frame) {
            const absPath = frame.filename;
            if(!absPath) return frame;
            frame.filename = `/${path.relative(__dirname, absPath).replace(/\\/g, "/")}`
            return frame;
          }
        }),
      ],
      beforeSend: (a,b) => {
        console.log("Sending Error to Sentry...")
        return a;
      },
      beforeBreadcrumb: (a,b) => {
        console.log(`Sending Breadcrumb to Sentry: ${a.message}`)
        return a;
      },
      beforeSendTransaction: () => null,
      debug: true,
})

caller(async()=>{
  await withIsolationScope(async (scope) => {
        scope.setUser({
            id: 0,
            username: "ROOT",
            Priv: "SuperUser"
        })
        console.log("HIII");
        throw new Error("TESTING!!!!"); // setUser ctx will not be attached
        //captureException(new Error("TESTING!!!!")); // setUser ctx will be attached
    })
});

Expected Result

Unhandled exception are expected to attach user info context that's wrapped around by the withScope

Actual Result

The given context doesn't show up on the dashboard, the workaround is to force all errors to pass through captureException (which then it wouldn't be unhandled).

Activity

  1. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Jun 2, 2025
  2. s1gr1d commented on Jun 3, 2025

    @s1gr1d
    Member

    Hello, we are aware of this and this is documented in the tests here.

    The issue you're experiencing is that thrown errors caught by global error handlers lose scope data from the active scope where the error was originally thrown.

    The problem occurs because of how and when the error gets captured:

    1. Manual captureException: When you call captureException(error) directly inside the withScope callback, it immediately captures the exception using the current active scope at that exact moment. The scope context is available and applied.
    2. Thrown errors: When you throw new Error(), the error bubbles up and gets caught by Sentry's global error handlers. By the time these global handlers execute, you're outside the scope context that was active when the error was thrown.

    You can use withIsolationScope here as this does not fork the scopes as frequently as withScope does. Here are the docs for the scopes: https://docs.sentry.io/platforms/javascript/guides/node/enriching-events/scopes/#different-kinds-of-scopes

  3. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Jun 3, 2025
  4. zhiyan114 commented on Jun 3, 2025

    @zhiyan114
    ContributorAuthor

    You can use withIsolationScope here as this does not fork the scopes as frequently as withScope does. Here are the docs for the scopes: https://docs.sentry.io/platforms/javascript/guides/node/enriching-events/scopes/#different-kinds-of-scopes

    Unfortunately, similar issue also happens when using withIsolationScope. Using the scope fork outside of the mentioned function will allow the forked scope to attach context normally.

  5. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Jun 3, 2025
  6. changed the title [-]withScope failed to capture context for unhandled exception inside setInterval[/-] [+]withIsolationScope failed to capture context for unhandled exception inside setInterval[/+] on Jun 3, 2025
  7. Lms24 commented on Jun 4, 2025

    @Lms24
    Member

    Hey @zhiyan114!

    I tried your reproduction and I can reproduce this but unfortunately, this is expected behaviour. Very similarly to what @s1gr1d explained, also the scope of withIsolationScope is only valid within the callback closure. Since the error is bubbling up to the global handler, we're exiting again.

    So the question left to answer is, why this works in the test Sigrid linked above. This is not very obvious but there's an important difference: In the test, we don't fork the isolation scope (like when using withIsolationScope) but we actually set the data on the pre-existing isolation scope. This isolation scope is set by our httpIntegration once the request comes in. This this test is using express, and therefore the errorHandler of express actually captures the error and this still happens within the request life cycle. Since the isolation scope is applied outside of the "express realm", it is still active when our express error handler calls captureException.

    I don't know much about your project but I'd recommend trying to do the following for your case:

    • avoid forking the isolation scope but setting your data on the already active isolation scope via getIsolationScope().
    • ^ implies that you have some kind of request/response http lifecycle on your server, where we already create a new isolation scope as early as possible automatically. If you don't have this, make sure to call withIsolationScope as early as possible yourself and ensure that you call captureException yourself.

    I opened #16479 to demonstrate that calling withScope in an express app also doesn't retain the data set on that scope on uncaught errors.

    Side note: We have discussed being a bit smarter about collecting scope data along the outer borders of scope closures so that this data is not lost but actually kept. There are good reasons for doing as well as not doing it. I'll bring this up with the team again but can't promise that we'll make changes.

    related: #11133

  8. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Jun 4, 2025
  9. 10 remaining items

  10. Lms24 commented on Jun 5, 2025

    @Lms24
    Member

    try/catching and calling captureException within your withIsolationScope closure is one option, yes.

    If you can tell me more about how your application works (e.g. does it handle requests? is there some kind of request/response life cycle?) maybe there's a better way.

    If your application does not handle multiple requests, users or other kinds of parallel tasks, you can also set whatever data you want on the global scope. Sentry.getGlobalScope().set... works exactly like the isolation and current scopes, just that the data put onto the global scope is added to every event.

  11. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Jun 5, 2025
  12. zhiyan114 commented on Jun 5, 2025

    @zhiyan114
    ContributorAuthor

    If you can tell me more about how your application works (e.g. does it handle requests? is there some kind of request/response life cycle?) maybe there's a better way.

    My application does have request/response cycle, but not in a way a web server would do:

    • There's several different eventEmitter from the library that I'm using to receive different events. Some event contains user request (which is where the withScope comes in play), while others don't.
    • iirc how the library works, it receives user request from websocket, which then makes a separate HTTP request to pull full information before the event is invoked. For response, another HTTP request is made.
  13. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Jun 5, 2025
  14. mydea commented on Jun 10, 2025

    @mydea
    Member

    You'll probably want to wrap every event emitter callback in a new isolation scope, e.g. (pseudo code incoming) something like this:

    emitter.on('event', () => {
      Sentry.withIsolationScope(() => {
        // actual code goes in here...
      });
    });

    That way everything happening inside of that emitter callback should be isolated.

  15. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Jun 10, 2025
  16. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Jun 10, 2025
  17. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Jun 10, 2025
  18. zhiyan114 commented on Jun 10, 2025

    @zhiyan114
    ContributorAuthor

    You'll probably want to wrap every event emitter callback in a new isolation scope, e.g. (pseudo code incoming) something like this:

    emitter.on('event', () => {
    Sentry.withIsolationScope(() => {
    // actual code goes in here...
    });
    });
    That way everything happening inside of that emitter callback should be isolated.

    Will give this a try! As for setInterval, I'll just use try/catch since setting withIsolationScope doesn't work within the code when unhandled error throws.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions