Post-Fibers: can core server packages ship native async instead of transpiled generators? (async stack traces)

I hit a debugging wall in a Meteor 3.4 app and want to sanity-check the build-target reasoning before I open an issue, in case I’m missing something obvious.

A publication threw on the server. Here’s the entire stack trace that reached my error tracker:

TypeError: Cannot read properties of null (reading 'id')
  at asyncGeneratorStep (programs/server/packages/ddp-server.js:255)
  at _throw (programs/server/packages/ddp-server.js:276)
  at Subscription.error (packages/ddp-server/livedata_server.js:970)
  ... (third-party APM wrapper frames)

That’s it. Every frame is the DDP generator trampoline or a wrapper. The two things I actually needed — which publication threw, and the original error underneath the tracking path aren’t in it. The async parent chain is gone before the trace is even captured.

Here’s my read on why, and I’d like to know if it’s right. ddp-server ships to Atmosphere already transpiled: async/await lowered to generators through Babel’s _asyncToGenerator/asyncGeneratorStep helper. V8’s zero-cost async stack traces only stitch frames across a native await. Route execution through a hand-rolled generator trampoline instead and there’s nothing left for V8 to attach the parent frames to. So on Node 22 the runtime is capable of good async traces, but the shipped generator lowering throws that away before it can.

The part I don’t understand: Fibers is gone in 3.0. I’d assumed that coroutine model was the main reason server code went through a generator-shaped transform in the first place. So why does the os build still do it?

Concretely:

  1. Is it a shared Babel preset across the web.browser.legacy and os archs, a conservative Node baseline for the server target, or something load-bearing I can’t see?
  2. Is there any supported way for an app author to opt their own server bundle out of the async-to-generator transform? I went looking and couldn’t find a knob.
  3. If someone retargeted the core server build to native async on Node ≥18, what breaks? Microtask-timing differences between the trampoline and native await are the thing I’d worry about.
  4. Has anyone actually measured the perf or bundle delta? I’d guess native is marginally faster and smaller, but honestly I care about the stack traces far more than the nanoseconds.

The payoff I’m after is real traces for server errors: publication throws, observer-callback failures, method rejections, instead of everything bottoming out at asyncGeneratorStep with no app context. If there’s interest I’m happy to benchmark or test a build.

(The specific null-deref above turned out to be a missing guard in the APM package, already patched. This isn’t about that bug — it’s about why a normal server error gives me a trace I can’t use.)

3 Likes

Great post — I had an AI agent go read the actual shipped bytes (published isopacks + freshly-built app bundles for 3.4 and 3.5) plus the compiler source, and run a control experiment. The symptom you’re seeing is 100% real, but the cause turned out to be slightly different from the Fibers-leftover theory. The good news: it’s fixable app-side today.

TL;DR: The published ddp-server isopack ships native async. It’s the app compile step that re-lowers it — Meteor 3.4+ uses SWC (enabled by the default "modern": true), and SWC is configured with jsc.target: 'es2015' for the server arch. Compiling ES2017 async/await down to es2015 forces the generator trampoline (_async_to_generator / asyncGeneratorStep), which is what destroys V8’s zero-cost async stack traces.

What the agent found in each artifact:

Artifact Subscription._runHandler generator helpers
Published isopack ddp-server/*/os/livedata_server.js async function() — native 0
App bundle, default 3.4/3.5 (meteor run and meteor build) function(){ return _async_to_generator(function*(){…}) } asyncGeneratorStep present
App bundle with meteor.modern: false async function() — native 0

So the Fibers-era Babel async-to-generator path really is dead (it’s gated on useNativeAsyncAwait = Meteor.isFibersDisabled, and Fibers is off) — a good instinct to suspect that area, but the isopack itself is clean. The re-lowering happens later.

The exact line — in babel-compiler’s SWC setup:

jsc: {
  ...(!isLegacyWebArch && { target: 'es2015' }),   // isLegacyWebArch = arch.includes('legacy')

The server os.* arch is not “legacy”, so it gets target: 'es2015'. Node LTS supports native async/await, so lowering it there is pure downside: broken async stack traces (and a small perf cost) for no compatibility gain.

Control experiment that confirmed causation: take a fresh 3.5 app, flip "modern": false in package.json, rebuild → _runHandler is back to native async function() with zero generator helpers. Flip it back → the trampoline returns.

One thing worth checking on your side: the error itself isn’t actually lost. Subscription.error_stopSubscriptionwrapInternalException does log the real thing server-side:

Meteor._debug("Exception from sub " + subName + " id " + subId, exception.stack)

So the publication name and the original stack should be in your server logs — the client deliberately only receives a sanitized Meteor.Error(500, "Internal server error"). If your APM only shows you the client-facing/wrapped error, the useful one may already be sitting in stdout.

Fixes, cheapest first:

  1. Add an app-level .swcrc (or swc.config.js) and bump the target — the compiler deep-merges jsc.target from your config:
{ "jsc": { "target": "es2022" } }

This restores native async on the server and your zero-cost async traces.
2. Or exclude core packages from SWC via meteor.modern.transpiler.excludePackages.
3. Or "modern": false (bigger hammer — turns SWC off entirely).

There’s probably also a legitimate upstream bug here worth an issue: the SWC server target shouldn’t be es2015 for os.* archs. Framed that way (“raise the server SWC target to es2017+”) it’s a clean one-line fix; framed as “the os arch transpiles async by default” it won’t survive review, since the isopack is demonstrably native.

Yeah you’re spot on @permb !

Looks like the fix might be coming in 3.5.1 Bump SWC server target to es2022 by mvogttech · Pull Request #14369 · meteor/meteor · GitHub