Skip to content

Conversation

@SyuTingSong
Copy link

Preserve Context Information for Exceptions Thrown Within Scopes

User Story

Laravel's Context::scope() is an excellent feature that allows developers to temporarily add contextual information for a specific operation, automatically cleaning up afterwards. This is particularly useful for scenarios like processing data one-by-one under different user contexts, batch operations, or switching between different operational contexts while maintaining clean separation.

However, there's a critical issue when exceptions are thrown within a scope: the finally block restores the previous context state before the exception bubbles up to the error handler. This means that ErrorHandler loses access to the valuable contextual information that was present when the exception actually occurred.

The Problem

Consider this scenario where you're processing users and their vendors in nested scopes:

public function sync()
{
    Context::add('batch_id', 'vendor_sync_2024');

    foreach ($users as $user) {
        try {
            Context::scope($this->processUserIntegrations(...), [
                'user_id' => $user->id,
                'user_email' => $user->email,
            ], [$user]);
        } catch (CredentialException $e) {
            // At this point, Context has been restored
            // ErrorHandler only sees 'batch_id', missing crucial user and vendor context
            Log::error('Vendor sync failed', Context::all()); // Missing user_id, vendor_id, operation!
        }
    }
}

private function processUserIntegrations(User $user)
{
    foreach ($user->integrations as $integrationVendor) {
        Context::scope($this->syncVendorData(...), [
            'vendor_id' => $integrationVendor->id,
            'vendor_name' => $integrationVendor->name,
            'operation' => 'credential_validation',
        ], ['integrationVendor' => $integrationVendor]);
    }
}

private function syncVendorData()
{
    $integrationVendor = Context::getHidden('integrationVendor');
    
    // Log automatically includes Context data: batch_id, user_id, user_email, vendor_id, etc.
    Log::info('Start syncing vendor integration');
    
    // Exception thrown from API call during credential validation
    $integrationVendor->callApi();
    
    // Save data for user
}

When the exception is caught and logged, the error handler cannot access the nested context information (user_id, user_email, vendor_id, vendor_name, operation) that would be invaluable for identifying which specific user and vendor combination failed.

Proposed Solution

This PR introduces a mechanism to preserve context information for exceptions thrown within scopes:

  1. Context Capture: When an exception is thrown within Context::scope(), we capture the current context state before the finally block restores it.

  2. WeakMap Association: Uses a WeakMap to associate exceptions with their context state when thrown within a scope, ensuring no memory leaks—when an exception's reference count drops to 0, the associated context can also be garbage collected.

  3. Context Retrieval: Adds a new Context::for(Throwable $exception) method that allows retrieving the preserved context for a specific exception.

Benefits

  • Enhanced Debugging: Error handlers can now access the complete context information present when exceptions occurred
  • Zero Breaking Changes: Existing functionality remains unchanged; this is purely additive
  • Memory Safe: Uses WeakMap to prevent memory leaks from exception references
  • Automatic Fallback: If no context is preserved for an exception, falls back to the current global context

Usage Example

public function sync()
{
    Context::add('batch_id', 'vendor_sync_2024');

    foreach ($users as $user) {
        try {
            Context::scope($this->processUserIntegrations(...), [
                'user_id' => $user->id,
                'user_email' => $user->email,
            ], [$user]);
        } catch (CredentialException $e) {
            $exceptionContext = Context::for($e);
            // Now has access to: batch_id, user_id, user_email, vendor_id, vendor_name, operation
            Log::error('Vendor sync failed', $exceptionContext->all());
        }
    }
}

This enhancement makes building robust, debuggable web applications significantly easier by ensuring critical contextual information is never lost during exception handling, especially in complex nested processing scenarios.

Future Considerations

Building on this foundation, I'm considering two additional enhancements:

  1. Enhanced ContextLogProcessor: Modify the ContextLogProcessor to automatically restore exception context when an 'exception' is present in the log context, following PSR-3 logging standards for exception handling, eliminating the need for manual Context::for($exception) calls in most logging scenarios.

  2. Third-party Integration: Submit pull requests to exception monitoring platforms like Sentry to enhance their Laravel integration, enabling automatic recovery of exception context data for richer error reporting and debugging capabilities.

These future improvements would make the context preservation even more seamless and extend the benefits to the broader Laravel ecosystem.

- Use WeakMap to associate exceptions with their scope context
- Capture context state when exceptions are thrown within scopes
- Add for() method to retrieve context for specific throwables
- Include comprehensive test coverage for exception context handling
Comment on lines +583 to +586
public function for(Throwable $e)
{
return self::$forThrowable[$e] ?? $this;
}
Copy link
Contributor

@cosmastech cosmastech Jul 22, 2025

Choose a reason for hiding this comment

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

thought: Quite a bit of synchronicity as we are working on recording contextual information in DataDog error tracking.

I think that the developer ergonomics here are less than ideal. Any time I want to report my exception (or let the container report it), I have to remember to reset the context from the exception. It feels more practical that the container would manage restoring the Context state for the exception.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe this can be done inside of Illuminate\Support\Foundation\Exceptions\Handler@exceptionContext()?

Copy link
Author

Choose a reason for hiding this comment

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

I completely agree that the exception Context retrieval should be built into framework public methods like Handler, Log, dd(), etc., without requiring developers to do anything manually. I didn't include those changes in the current PR mainly because I wanted to avoid making the changeset too large, and wanted to gauge the maintainers' attitude before deciding whether to complete everything in a single PR.

@taylorotwell
Copy link
Member

Thanks for your pull request to Laravel!

Unfortunately, I'm going to delay merging this code for now. To preserve our ability to adequately maintain the framework, we need to be very careful regarding the amount of code we include.

If applicable, please consider releasing your code as a package so that the community can still take advantage of your contributions!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants