-
-
Notifications
You must be signed in to change notification settings - Fork 522
Description
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
- PR Avoid putting symbols in global to fix incompatibility with Temporal Sandbox. #4196 - Previous Temporal compatibility fix (global symbol pollution)
- Temporal sandbox documentation: https://docs.temporal.io/develop/typescript/core-application
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
What is the expected behavior?
No response
What do you see instead?
No response
Additional information
No response