Skip to content

Effect violating sandbox constraints when run inside temporal worker #5986

@validkeys

Description

@validkeys

What version of Effect is running?

Latest

What steps can reproduce the bug?

Bug: Effect incompatible with Temporal workflow sandbox due to Error.stackTraceLimit assignments

Summary

Effect cannot be used in Temporal TypeScript workflow code because Effect directly assigns to Error.stackTraceLimit, which is read-only in Temporal's sandboxed workflow environment. This causes an immediate runtime error when Effect is imported.

Environment

  • Effect version: 3.19.14 (also tested with 3.14.7)
  • Node.js version: 22.x
  • Temporal SDK version: 1.14.1
  • OS: macOS (also reproducible on Linux)

Description

Temporal runs workflow code in a sandboxed V8 isolate to ensure determinism. In this sandbox, certain global properties are made read-only, including Error.stackTraceLimit. Effect modifies this property in approximately 30 places throughout the codebase for stack trace management purposes.

When Effect is imported in a Temporal workflow, the following error occurs:

Cannot assign to read only property 'stackTraceLimit' of function 'class SandboxedError extends Error { ... }'

Note on Previous Temporal Fix

PR #4196 (commit 5c67bdc) addressed a different Temporal compatibility issue by avoiding putting symbols in globalThis. That fix resolved symbol pollution but did not address the stackTraceLimit issue, which is a separate problem.

Reproduction

Minimal reproduction (simulated sandbox)

// test-effect-sandbox.cjs
// Simulate Temporal sandbox - make Error.stackTraceLimit read-only
Object.defineProperty(Error, 'stackTraceLimit', {
  value: Error.stackTraceLimit,
  writable: false,
  configurable: false
});

console.log('Error.stackTraceLimit is now read-only');

try {
  const Effect = require('effect');
  console.log('SUCCESS: Effect loaded without errors');
} catch (error) {
  console.log('FAILURE:', error.message);
}

Output:

Error.stackTraceLimit is now read-only
FAILURE: Cannot assign to read only property 'stackTraceLimit' of function 'class Error { [native code] }'

Full reproduction with Temporal

// workflows.ts
import { Effect, pipe } from 'effect';

export async function myWorkflow(input: string): Promise<string> {
  const program = pipe(
    Effect.succeed(input),
    Effect.map((s) => s.toUpperCase())
  );
  return Effect.runPromise(program);
}
// worker.ts
import { Worker } from '@temporalio/worker';

const worker = await Worker.create({
  workflowsPath: require.resolve('./workflows'),
  taskQueue: 'test',
});

await worker.run();

Result: Worker fails to start with stackTraceLimit error during workflow bundling/execution.

Affected Code Locations

The following files contain direct stackTraceLimit assignments:

File Occurrences Pattern
src/Effect.ts 12 Error.stackTraceLimit = 2
src/internal/runtime.ts 6 Error.stackTraceLimit = 0
src/internal/context.ts 4 Error.stackTraceLimit = 2
src/internal/tracer.ts 2 Error.stackTraceLimit = 3
src/Micro.ts 2 globalThis.Error.stackTraceLimit = 2
src/LayerMap.ts 2 Err.stackTraceLimit = 2
Other internal files ~2 Various

Common pattern in the codebase:

const limit = Error.stackTraceLimit;
Error.stackTraceLimit = 2;  // <-- This line throws in Temporal sandbox
const error = new Error();
Error.stackTraceLimit = limit;

Expected Behavior

Effect should handle environments where Error.stackTraceLimit is read-only without throwing an error. The stack trace optimization is a performance enhancement and should fail gracefully.

Proposed Solution

Wrap stackTraceLimit assignments in try/catch blocks:

Before:

const limit = Error.stackTraceLimit;
Error.stackTraceLimit = 2;
const error = new Error();
Error.stackTraceLimit = limit;

After:

const limit = Error.stackTraceLimit;
try { Error.stackTraceLimit = 2; } catch {}
const error = new Error();
try { Error.stackTraceLimit = limit; } catch {}

This maintains the optimization in standard environments while gracefully degrading in restricted environments like Temporal's sandbox.

Alternative: Check writability first

const stackTraceLimitWritable = Object.getOwnPropertyDescriptor(Error, 'stackTraceLimit')?.writable !== false;

// Later in code:
if (stackTraceLimitWritable) {
  Error.stackTraceLimit = 2;
}

This approach avoids the try/catch overhead but requires a one-time check at module initialization.

Current Workaround

Users can work around this issue using a Temporal webpack plugin that transforms the bundled code:

class StackTraceLimitTransformPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('StackTraceLimitTransformPlugin', (compilation, callback) => {
      for (const [filename, asset] of Object.entries(compilation.assets)) {
        if (filename.endsWith('.js')) {
          let source = asset.source();
          source = source
            .replace(
              /globalThis\.Error\.stackTraceLimit\s*=\s*([^;,\n\r)]+)/g,
              '(function(){try{globalThis.Error.stackTraceLimit=$1}catch(e){}})() '
            )
            .replace(
              /(?<![.\w])Error\.stackTraceLimit\s*=\s*([^;,\n\r)]+)/g,
              '(function(){try{Error.stackTraceLimit=$1}catch(e){}})() '
            )
            .replace(
              /Err\.stackTraceLimit\s*=\s*([^;,\n\r)]+)/g,
              '(function(){try{Err.stackTraceLimit=$1}catch(e){}})() '
            );
          compilation.assets[filename] = {
            source: () => source,
            size: () => source.length,
          };
        }
      }
      callback();
    });
  }
}

// In worker configuration:
const worker = await Worker.create({
  workflowsPath: require.resolve('./workflows'),
  taskQueue: 'my-queue',
  bundlerOptions: {
    webpackConfigHook: (config) => {
      config.plugins = config.plugins || [];
      config.plugins.push(new StackTraceLimitTransformPlugin());
      return config;
    },
  },
});

This workaround successfully transforms all 30 stackTraceLimit assignments in the Effect bundle, allowing Effect to work in Temporal workflows.

Impact

This issue prevents using Effect in Temporal workflows, which limits adoption for teams that want to combine:

  • Effect's powerful composition and error handling
  • Temporal's durable execution and workflow orchestration

Temporal is a popular workflow engine with growing adoption, and Effect compatibility would benefit both communities.

Related Issues

Additional Context

A full POC demonstrating the issue and workaround is available attached (sorry it's a zip)

The POC includes:

  • Simulated sandbox test confirming the issue
  • Temporal worker configurations for testing
  • Webpack plugin workaround
  • Bundle analysis showing transformation results

Archive.zip

What is the expected behavior?

No response

What do you see instead?

No response

Additional information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions