Skip to content

fix: use types.isNativeError() for cross-VM Error serialization#1164

Merged
TooTallNate merged 1 commit intomainfrom
02-23-fix_use_types.isnativeerror_for_cross-vm_error_serialization
Feb 23, 2026
Merged

fix: use types.isNativeError() for cross-VM Error serialization#1164
TooTallNate merged 1 commit intomainfrom
02-23-fix_use_types.isnativeerror_for_cross-vm_error_serialization

Conversation

@TooTallNate
Copy link
Copy Markdown
Member

FatalError was not properly serialized when passed from workflow code into a step function because the Error reducer checked value instanceof global.Error where global is the VM's globalThis. Errors created in the host context (like FatalError from @workflow/errors) have a different Error prototype than the VM context, so the instanceof check returned false and the error was silently dropped.

Replaced with types.isNativeError() from node:util which uses V8's internal type tag and works across VM context boundaries.

FatalError was not properly serialized when passed from workflow code into a step function because the Error reducer checked `value instanceof global.Error` where `global` is the VM's globalThis. Errors created in the host context (like FatalError from @workflow/errors) have a different Error prototype than the VM context, so the instanceof check returned false and the error was silently dropped.

Replaced with `types.isNativeError()` from `node:util` which uses V8's internal type tag and works across VM context boundaries.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 23, 2026

🦋 Changeset detected

Latest commit: 825bbe8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@workflow/core Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
workflow Patch
@workflow/world-testing Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@TooTallNate TooTallNate marked this pull request as ready for review February 23, 2026 19:40
Copilot AI review requested due to automatic review settings February 23, 2026 19:40
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Feb 23, 2026

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 23, 2026

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.033s (+1.2%) 1.005s (~) 0.972s 10 1.00x
💻 Local Express 0.033s (+4.7%) 1.005s (~) 0.972s 10 1.02x
💻 Local Next.js (Turbopack) 0.043s (-3.2%) 1.005s (~) 0.962s 10 1.31x
🌐 Redis Next.js (Turbopack) 0.048s (+28.0% 🔺) 1.005s (~) 0.957s 10 1.47x
🐘 Postgres Next.js (Turbopack) 0.048s 1.011s 0.962s 10 1.48x
🐘 Postgres Nitro 0.052s (-0.6%) 1.009s (~) 0.957s 10 1.59x
🐘 Postgres Express 0.053s (-0.6%) 1.011s (~) 0.958s 10 1.63x
🌐 MongoDB Next.js (Turbopack) 0.093s (+49.2% 🔺) 1.008s (~) 0.915s 10 2.87x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 0.511s (+15.6% 🔺) 1.897s (+3.7%) 1.386s 10 1.00x
▲ Vercel Express 0.536s (+10.3% 🔺) 1.944s (-1.7%) 1.408s 10 1.05x
▲ Vercel Nitro 0.555s (-10.9% 🟢) 1.972s (-5.2% 🟢) 1.416s 10 1.09x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Next.js (Turbopack) 1.098s (~) 2.005s (~) 0.907s 10 1.00x
🌐 Redis Next.js (Turbopack) 1.107s (+2.4%) 2.006s (~) 0.899s 10 1.01x
💻 Local Express 1.107s (~) 2.005s (~) 0.898s 10 1.01x
💻 Local Nitro 1.109s (~) 2.006s (~) 0.897s 10 1.01x
🐘 Postgres Next.js (Turbopack) 1.122s 2.011s 0.889s 10 1.02x
🐘 Postgres Express 1.124s (~) 2.010s (~) 0.886s 10 1.02x
🐘 Postgres Nitro 1.129s (~) 2.010s (~) 0.881s 10 1.03x
🌐 MongoDB Next.js (Turbopack) 1.318s (+1.9%) 2.008s (~) 0.691s 10 1.20x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.096s (+4.9%) 3.482s (+12.7% 🔺) 1.386s 10 1.00x
▲ Vercel Nitro 2.112s (+1.5%) 3.340s (-1.6%) 1.228s 10 1.01x
▲ Vercel Next.js (Turbopack) 2.138s (-34.6% 🟢) 3.457s (-18.7% 🟢) 1.319s 10 1.02x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 10.695s (+1.0%) 11.022s (~) 0.327s 3 1.00x
💻 Local Next.js (Turbopack) 10.700s (~) 11.021s (~) 0.322s 3 1.00x
💻 Local Nitro 10.834s (~) 11.022s (~) 0.188s 3 1.01x
🐘 Postgres Next.js (Turbopack) 10.836s 11.041s 0.205s 3 1.01x
💻 Local Express 10.843s (~) 11.023s (~) 0.179s 3 1.01x
🐘 Postgres Nitro 10.890s (~) 11.037s (~) 0.147s 3 1.02x
🐘 Postgres Express 10.901s (+0.6%) 11.043s (~) 0.141s 3 1.02x
🌐 MongoDB Next.js (Turbopack) 12.259s (+0.6%) 13.018s (~) 0.759s 3 1.15x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 16.258s (-7.6% 🟢) 17.734s (-6.2% 🟢) 1.476s 2 1.00x
▲ Vercel Next.js (Turbopack) 16.267s (+0.9%) 17.293s (+1.2%) 1.026s 2 1.00x
▲ Vercel Express 16.427s (-7.8% 🟢) 17.331s (-8.2% 🟢) 0.903s 2 1.01x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 26.858s (+1.3%) 27.048s (~) 0.190s 3 1.00x
🐘 Postgres Next.js (Turbopack) 27.149s 28.065s 0.916s 3 1.01x
🐘 Postgres Nitro 27.153s (~) 28.050s (~) 0.897s 3 1.01x
💻 Local Next.js (Turbopack) 27.173s (~) 28.050s (~) 0.877s 3 1.01x
🐘 Postgres Express 27.185s (~) 28.062s (~) 0.877s 3 1.01x
💻 Local Nitro 27.498s (~) 28.051s (~) 0.553s 3 1.02x
💻 Local Express 27.527s (~) 28.052s (~) 0.525s 3 1.02x
🌐 MongoDB Next.js (Turbopack) 30.421s (~) 31.033s (~) 0.612s 2 1.13x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 41.165s (+4.2%) 42.343s (+3.0%) 1.178s 2 1.00x
▲ Vercel Next.js (Turbopack) 41.244s (+4.6%) 42.659s (+5.9% 🔺) 1.415s 2 1.00x
▲ Vercel Express 42.105s (+3.9%) 43.576s (+4.8%) 1.472s 2 1.02x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 54.297s (+1.2%) 55.096s (+1.8%) 0.799s 2 1.00x
🐘 Postgres Nitro 54.987s (~) 55.084s (-0.9%) 0.097s 2 1.01x
🐘 Postgres Next.js (Turbopack) 55.002s 55.102s 0.100s 2 1.01x
🐘 Postgres Express 55.049s (~) 55.102s (~) 0.053s 2 1.01x
💻 Local Next.js (Turbopack) 56.482s (~) 57.100s (~) 0.618s 2 1.04x
💻 Local Express 57.311s (~) 58.102s (~) 0.791s 2 1.06x
💻 Local Nitro 57.350s (~) 58.106s (~) 0.756s 2 1.06x
🌐 MongoDB Next.js (Turbopack) 60.962s (~) 61.069s (~) 0.107s 2 1.12x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 82.143s (-0.8%) 83.286s (-1.5%) 1.143s 2 1.00x
▲ Vercel Next.js (Turbopack) 83.096s (~) 83.924s (~) 0.829s 2 1.01x
▲ Vercel Express 84.499s (+2.2%) 85.760s (+3.1%) 1.261s 2 1.03x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 1.257s (+5.5% 🔺) 2.006s (~) 0.749s 15 1.00x
🐘 Postgres Nitro 1.340s (~) 2.010s (~) 0.670s 15 1.07x
🐘 Postgres Express 1.358s (~) 2.010s (~) 0.652s 15 1.08x
🐘 Postgres Next.js (Turbopack) 1.379s 2.010s 0.631s 15 1.10x
💻 Local Express 1.415s (~) 2.006s (~) 0.591s 15 1.13x
💻 Local Nitro 1.417s (~) 2.005s (~) 0.589s 15 1.13x
💻 Local Next.js (Turbopack) 1.421s (~) 2.005s (~) 0.584s 15 1.13x
🌐 MongoDB Next.js (Turbopack) 2.180s (+1.0%) 3.008s (~) 0.828s 10 1.73x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.135s (-7.9% 🟢) 3.453s (+8.2% 🔺) 1.318s 9 1.00x
▲ Vercel Nitro 2.207s (+2.0%) 3.563s (+2.8%) 1.356s 9 1.03x
▲ Vercel Express 2.239s (+7.5% 🔺) 3.591s (+17.9% 🔺) 1.351s 9 1.05x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.891s (-2.8%) 2.398s (~) 0.507s 13 1.00x
🐘 Postgres Express 2.012s (+3.9%) 2.512s (~) 0.500s 12 1.06x
🐘 Postgres Next.js (Turbopack) 2.022s 2.517s 0.495s 12 1.07x
💻 Local Next.js (Turbopack) 2.421s (-1.6%) 3.007s (~) 0.586s 10 1.28x
🌐 Redis Next.js (Turbopack) 2.465s (+4.8%) 3.007s (~) 0.543s 10 1.30x
💻 Local Nitro 2.578s (-0.7%) 3.007s (~) 0.429s 10 1.36x
💻 Local Express 2.590s (~) 3.008s (~) 0.418s 10 1.37x
🌐 MongoDB Next.js (Turbopack) 4.682s (+0.6%) 5.177s (~) 0.495s 6 2.48x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.594s (+5.3% 🔺) 3.699s (+3.3%) 1.105s 9 1.00x
▲ Vercel Express 2.616s (+1.5%) 3.837s (+10.4% 🔺) 1.221s 8 1.01x
▲ Vercel Next.js (Turbopack) 2.788s (-23.3% 🟢) 3.884s (-16.0% 🟢) 1.095s 8 1.07x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 3.259s (+1.4%) 4.025s (~) 0.766s 8 1.00x
🐘 Postgres Nitro 3.521s (-4.3%) 4.308s (~) 0.787s 7 1.08x
🐘 Postgres Next.js (Turbopack) 3.554s 4.265s 0.711s 8 1.09x
🌐 Redis Next.js (Turbopack) 4.049s (+5.8% 🔺) 4.580s (+14.2% 🔺) 0.532s 7 1.24x
💻 Local Next.js (Turbopack) 6.966s (+4.9%) 7.515s (+4.2%) 0.548s 4 2.14x
💻 Local Nitro 7.337s (-3.2%) 8.023s (~) 0.686s 4 2.25x
💻 Local Express 7.910s (+5.4% 🔺) 8.274s (+3.2%) 0.363s 4 2.43x
🌐 MongoDB Next.js (Turbopack) 10.018s (+3.0%) 10.350s (~) 0.332s 3 3.07x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.147s (+3.6%) 4.118s (+6.6% 🔺) 0.971s 8 1.00x
▲ Vercel Next.js (Turbopack) 3.356s (-1.3%) 4.376s (+0.9%) 1.020s 7 1.07x
▲ Vercel Nitro 3.413s (-3.2%) 4.533s (-5.2% 🟢) 1.120s 7 1.08x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 1.258s (+5.5% 🔺) 2.006s (~) 0.748s 15 1.00x
🐘 Postgres Express 1.357s (-2.0%) 2.010s (~) 0.652s 15 1.08x
🐘 Postgres Nitro 1.361s (+0.7%) 2.009s (~) 0.648s 15 1.08x
🐘 Postgres Next.js (Turbopack) 1.363s 2.010s 0.647s 15 1.08x
💻 Local Next.js (Turbopack) 1.417s (-1.1%) 2.005s (~) 0.587s 15 1.13x
💻 Local Express 1.422s (~) 2.005s (~) 0.583s 15 1.13x
💻 Local Nitro 1.432s (-3.9%) 2.005s (-3.3%) 0.573s 15 1.14x
🌐 MongoDB Next.js (Turbopack) 2.143s (-2.1%) 3.007s (~) 0.864s 10 1.70x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 2.060s (-7.2% 🟢) 3.457s (+7.8% 🔺) 1.397s 9 1.00x
▲ Vercel Express 2.128s (+4.9%) 3.563s (+14.2% 🔺) 1.436s 9 1.03x
▲ Vercel Nitro 2.232s (+8.4% 🔺) 3.583s (~) 1.350s 9 1.08x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.896s (-0.8%) 2.322s (-3.2%) 0.426s 13 1.00x
🐘 Postgres Next.js (Turbopack) 1.901s 2.226s 0.326s 14 1.00x
🐘 Postgres Nitro 1.947s (~) 2.513s (~) 0.567s 12 1.03x
🌐 Redis Next.js (Turbopack) 2.488s (+4.7%) 3.007s (~) 0.519s 10 1.31x
💻 Local Next.js (Turbopack) 2.547s (+0.8%) 3.008s (~) 0.461s 10 1.34x
💻 Local Express 2.670s (~) 3.008s (~) 0.338s 10 1.41x
💻 Local Nitro 2.709s (~) 3.007s (~) 0.299s 10 1.43x
🌐 MongoDB Next.js (Turbopack) 4.655s (-2.1%) 5.175s (~) 0.520s 6 2.46x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 2.215s (-10.2% 🟢) 3.467s (-21.8% 🟢) 1.251s 9 1.00x
▲ Vercel Next.js (Turbopack) 2.336s (-39.9% 🟢) 3.451s (-25.3% 🟢) 1.115s 9 1.05x
▲ Vercel Express 2.425s (+10.2% 🔺) 3.613s (+15.8% 🔺) 1.189s 9 1.09x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 3.580s (-11.2% 🟢) 4.146s (-12.6% 🟢) 0.566s 8 1.00x
🐘 Postgres Express 3.598s (+6.6% 🔺) 4.307s (+3.7%) 0.709s 7 1.01x
🐘 Postgres Next.js (Turbopack) 3.740s 4.451s 0.711s 7 1.04x
🌐 Redis Next.js (Turbopack) 4.054s (+4.7%) 4.438s (+7.4% 🔺) 0.384s 7 1.13x
💻 Local Next.js (Turbopack) 7.037s (-6.2% 🟢) 7.518s (-6.2% 🟢) 0.482s 4 1.97x
💻 Local Nitro 7.814s (-1.9%) 8.278s (-2.9%) 0.464s 4 2.18x
💻 Local Express 7.942s (~) 8.273s (~) 0.331s 4 2.22x
🌐 MongoDB Next.js (Turbopack) 9.997s (+3.7%) 10.348s (~) 0.351s 3 2.79x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.414s (-11.8% 🟢) 3.693s (-0.5%) 1.279s 9 1.00x
▲ Vercel Nitro 2.768s (-32.5% 🟢) 3.856s (-25.7% 🟢) 1.088s 8 1.15x
▲ Vercel Next.js (Turbopack) 2.967s (+3.5%) 4.001s (+17.8% 🔺) 1.034s 8 1.23x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🌐 Redis 🥇 Next.js (Turbopack) 0.143s (+22.4% 🔺) 1.000s (~) 0.001s (-7.7% 🟢) 1.007s (~) 0.864s 10 1.00x
💻 Local Next.js (Turbopack) 0.143s (-2.8%) 1.001s (~) 0.011s (+2.9%) 1.016s (~) 0.872s 10 1.00x
💻 Local Nitro 0.175s (+1.3%) 1.002s (~) 0.011s (~) 1.016s (~) 0.841s 10 1.22x
💻 Local Express 0.176s (-1.6%) 1.002s (~) 0.011s (+2.8%) 1.016s (~) 0.841s 10 1.23x
🐘 Postgres Next.js (Turbopack) 0.179s 1.000s 0.001s 1.011s 0.832s 10 1.25x
🐘 Postgres Nitro 0.189s (-5.8% 🟢) 0.992s (~) 0.002s (+60.0% 🔺) 1.011s (~) 0.821s 10 1.32x
🐘 Postgres Express 0.204s (~) 0.990s (~) 0.001s (~) 1.011s (~) 0.806s 10 1.42x
🌐 MongoDB Next.js (Turbopack) 0.485s (~) 0.962s (+0.7%) 0.002s (~) 1.009s (~) 0.523s 10 3.39x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 1.586s (-38.2% 🟢) 1.913s (-36.7% 🟢) 0.161s (-1.4%) 2.603s (-29.1% 🟢) 1.017s 10 1.00x
▲ Vercel Express 1.687s (-9.6% 🟢) 2.090s (-9.5% 🟢) 0.576s (+278.3% 🔺) 3.202s (+12.2% 🔺) 1.515s 10 1.06x
▲ Vercel Next.js (Turbopack) 1.750s (+20.4% 🔺) 2.487s (+31.8% 🔺) 0.123s (-22.6% 🟢) 3.160s (+30.1% 🔺) 1.410s 10 1.10x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Next.js (Turbopack) 10/12
🐘 Postgres Next.js (Turbopack) 5/12
▲ Vercel Nitro 6/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 6/12
Next.js (Turbopack) 🌐 Redis 6/12
Nitro 🐘 Postgres 6/12
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

Copy link
Copy Markdown
Member Author

TooTallNate commented Feb 23, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 23, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 523 0 49 572
✅ 💻 Local Development 556 0 68 624
✅ 📦 Local Production 556 0 68 624
✅ 🐘 Local Postgres 556 0 68 624
✅ 🪟 Windows 49 0 3 52
❌ 🌍 Community Worlds 111 45 9 165
✅ 📋 Other 135 0 21 156
Total 2486 45 286 2817

❌ Failed Tests

🌍 Community Worlds (45 failed)

turso (45 failed):

  • addTenWorkflow
  • addTenWorkflow
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • hookWorkflow
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling retry behavior workflow completes despite transient 5xx on step_completed
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 47 0 5
✅ example 47 0 5
✅ express 47 0 5
✅ fastify 47 0 5
✅ hono 47 0 5
✅ nextjs-turbopack 50 0 2
✅ nextjs-webpack 50 0 2
✅ nitro 47 0 5
✅ nuxt 47 0 5
✅ sveltekit 47 0 5
✅ vite 47 0 5
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 45 0 7
✅ express-stable 45 0 7
✅ fastify-stable 45 0 7
✅ hono-stable 45 0 7
✅ nextjs-turbopack-canary 49 0 3
✅ nextjs-turbopack-stable 49 0 3
✅ nextjs-webpack-canary 49 0 3
✅ nextjs-webpack-stable 49 0 3
✅ nitro-stable 45 0 7
✅ nuxt-stable 45 0 7
✅ sveltekit-stable 45 0 7
✅ vite-stable 45 0 7
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 45 0 7
✅ express-stable 45 0 7
✅ fastify-stable 45 0 7
✅ hono-stable 45 0 7
✅ nextjs-turbopack-canary 49 0 3
✅ nextjs-turbopack-stable 49 0 3
✅ nextjs-webpack-canary 49 0 3
✅ nextjs-webpack-stable 49 0 3
✅ nitro-stable 45 0 7
✅ nuxt-stable 45 0 7
✅ sveltekit-stable 45 0 7
✅ vite-stable 45 0 7
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 45 0 7
✅ express-stable 45 0 7
✅ fastify-stable 45 0 7
✅ hono-stable 45 0 7
✅ nextjs-turbopack-canary 49 0 3
✅ nextjs-turbopack-stable 49 0 3
✅ nextjs-webpack-canary 49 0 3
✅ nextjs-webpack-stable 49 0 3
✅ nitro-stable 45 0 7
✅ nuxt-stable 45 0 7
✅ sveltekit-stable 45 0 7
✅ vite-stable 45 0 7
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 49 0 3
❌ 🌍 Community Worlds
App Passed Failed Skipped
✅ mongodb-dev 3 0 0
✅ mongodb 49 0 3
✅ redis-dev 3 0 0
✅ redis 49 0 3
✅ turso-dev 3 0 0
❌ turso 4 45 3
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 45 0 7
✅ e2e-local-postgres-nest-stable 45 0 7
✅ e2e-local-prod-nest-stable 45 0 7

📋 View full workflow run

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a critical bug where errors from the host context (like FatalError) were not properly serialized when passed to workflow code running in a VM context. The issue occurred because instanceof global.Error checks fail across VM boundaries since each context has its own Error constructor. The fix uses types.isNativeError() from Node.js's node:util module, which relies on V8's internal type tag and works across all VM contexts.

Changes:

  • Replaced instanceof global.Error with types.isNativeError() in the Error reducer
  • Added comprehensive test coverage for cross-VM Error serialization scenarios
  • Updated changeset to document the patch fix

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
packages/core/src/serialization.ts Updated Error reducer to use types.isNativeError() instead of instanceof global.Error with detailed explanatory comments
packages/core/src/serialization.test.ts Added three new test cases covering host-context errors, VM-context errors, and Error subclasses in cross-VM scenarios
.changeset/small-houses-walk.md Added changeset documenting the FatalError serialization fix

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@TooTallNate TooTallNate requested a review from a team February 23, 2026 19:50
@TooTallNate TooTallNate enabled auto-merge (squash) February 23, 2026 20:07
@TooTallNate TooTallNate merged commit d99ca9c into main Feb 23, 2026
166 of 169 checks passed
@TooTallNate TooTallNate deleted the 02-23-fix_use_types.isnativeerror_for_cross-vm_error_serialization branch February 23, 2026 20:08
TooTallNate added a commit that referenced this pull request May 1, 2026
Bundlers like Turbopack compile `export class FatalError extends Error
{...}` into a registration call like `e.s(["FatalError", 0, class
extends Error {...}])` — passing an anonymous class expression as a
function argument. The resulting constructor function has `name === ''`,
which broke the previous `value.constructor?.name === subclassName`
match: an instance of the bundled FatalError class no longer matched the
dedicated FatalError reducer and instead fell through to the generic
`Error` reducer, losing class identity across the workflow boundary.

This was caught by the local-prod CI matrix, where each Next.js route
gets its own bundled chunk: a real `new FatalError('fatal!')` returned
from a workflow was serialized as a plain Error and revived without
`instanceof FatalError` holding on the consumer side.

Switch the match in `reduceNamedErrorSubclassBase` to `value.name`,
which:
- works for built-in subclasses (TypeError/RangeError/… all set
  `name` automatically and aren't bundled, so behavior is unchanged
  in practice).
- works for FatalError/RetryableError, whose constructors set
  `this.name` explicitly — robust across realms AND bundlers.
- is consistent with how `FatalError.is()` / `RetryableError.is()`
  already identify their values.

Two existing cross-VM Error tests (added in #1164) used `name = 'FatalError'`
on a plain Error to stand in for any cross-realm error — which now hits
the dedicated FatalError reducer (returning a host-realm FatalError)
instead of the generic Error reducer (which constructs a VM-realm Error).
Renamed the stand-in to `'CustomError'` so they continue to exercise the
intended path.
TooTallNate added a commit that referenced this pull request May 1, 2026
…1513)

* Add WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE to FatalError and RetryableError

Add custom serialization methods to FatalError and RetryableError in
@workflow/errors, enabling the SWC plugin to discover and register them
through the standard class serialization pipeline. This preserves class
identity (instanceof), the fatal flag, and the retryAfter date when
these errors cross serialization boundaries.

- Add @workflow/serde dependency to @workflow/errors
- Add WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE static methods to both classes
- Add unit tests verifying Instance-based round-trip serialization
- Add e2e workflow tests verifying class identity preservation end-to-end

* Address review feedback on FatalError/RetryableError serde

- FatalError: preserve cause property when present (Copilot feedback)
- RetryableError: preserve cause property when present
- RetryableError: serialize retryAfter as numeric timestamp for realm
  safety (the Date reducer uses instanceof global.Date which fails
  across VM realms; timestamps sidestep that issue)
- Replace e2e tests with step return value serialization (step throw
  path always reconstructs errors as FatalError, so those tests don't
  exercise the new serde code path)
- Add unit tests for cause preservation on both classes

* Bump fatal-retryable-error-serialization changeset to minor

Adding WORKFLOW_SERIALIZE / WORKFLOW_DESERIALIZE hooks to FatalError
and RetryableError is a feature, not a bug fix.

* Switch FatalError/RetryableError to first-class serialization

Replace the WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE static methods on
FatalError and RetryableError with dedicated reducers/revivers in the
common serialization module. The Instance/Class pipeline relies on the
SWC plugin discovering classes and registering them by classId, which
means values constructed in environments that don't run the plugin
(vitest e2e runner, ad-hoc Node scripts) can't be deserialized.
Treating FatalError/RetryableError as first-class serialization targets
makes them round-trip from any environment with no setup, matching the
behavior of TypeError, RangeError, etc. added in the previous commit.

- Drop @workflow/serde dependency on @workflow/errors
- Remove WORKFLOW_SERIALIZE/DESERIALIZE statics from FatalError/RetryableError
- Add FatalError/RetryableError reducers to serialization/reducers/common.ts
  with cached base-reducer factories for the subclasses that wrap the
  shared shape (RetryableError, AggregateError)
- Migrate unit tests off registerSerializationClass setup
- Extend the errorSubclassRoundTripWorkflow e2e test to cover FatalError
  and RetryableError, and drop the parallel errorFatalSerdeRoundTrip /
  errorRetryableSerdeRoundTrip tests

* Address review feedback on FatalError/RetryableError serde

- Soundness: split makeErrorSubclassReducer into a shared base helper
  (reduceErrorBase / reduceNamedErrorSubclassBase returning the
  BaseErrorPayload shape) plus a thin wrapper constrained to subclass
  keys whose serialized shape is exactly that base payload. The
  AggregateError and RetryableError reducers — which extend the base
  with extra fields — now consume reduceNamedErrorSubclassBase
  directly instead of calling makeErrorSubclassReducer with an
  unsound type cast. The compiler now rejects accidental misuse
  (SimpleErrorSubclassKey type guard).
- Realm safety: RetryableError reviver constructs retryAfter via
  new global.Date(...) to match the rest of the module and ensure
  the resulting Date passes instanceof global.Date checks in the
  target realm.
- Test strength: assert serialized payloads contain the literal
  devalue marker ["FatalError",N] / ["RetryableError",N] rather
  than the bare class name (which would also match a generic Error
  payload whose name happens to be "FatalError"). Also assert the
  generic ["Error",N] marker is absent.

* Match Error subclass reducers by `value.name`, not constructor name

Bundlers like Turbopack compile `export class FatalError extends Error
{...}` into a registration call like `e.s(["FatalError", 0, class
extends Error {...}])` — passing an anonymous class expression as a
function argument. The resulting constructor function has `name === ''`,
which broke the previous `value.constructor?.name === subclassName`
match: an instance of the bundled FatalError class no longer matched the
dedicated FatalError reducer and instead fell through to the generic
`Error` reducer, losing class identity across the workflow boundary.

This was caught by the local-prod CI matrix, where each Next.js route
gets its own bundled chunk: a real `new FatalError('fatal!')` returned
from a workflow was serialized as a plain Error and revived without
`instanceof FatalError` holding on the consumer side.

Switch the match in `reduceNamedErrorSubclassBase` to `value.name`,
which:
- works for built-in subclasses (TypeError/RangeError/… all set
  `name` automatically and aren't bundled, so behavior is unchanged
  in practice).
- works for FatalError/RetryableError, whose constructors set
  `this.name` explicitly — robust across realms AND bundlers.
- is consistent with how `FatalError.is()` / `RetryableError.is()`
  already identify their values.

Two existing cross-VM Error tests (added in #1164) used `name = 'FatalError'`
on a plain Error to stand in for any cross-realm error — which now hits
the dedicated FatalError reducer (returning a host-realm FatalError)
instead of the generic Error reducer (which constructs a VM-realm Error).
Renamed the stand-in to `'CustomError'` so they continue to exercise the
intended path.
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