You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In a binary compiled with Perry (commit 32352733), the process exits silently — no JS error message, no stack trace, just a clean exit code that PM2 (or any supervisor) treats as a crash and restarts. The trigger is a pattern that should be a basic try/catch:
Expected: the throw is caught by the catch block, console.error runs.
Actual: the process dies. No log line from catch. The supervisor restarts the binary.
Real-world manifestation
A production service (gscmaster-api, Fastify+Perry) crashed every 30 minutes for 7 days (330 PM2 restarts) before the root cause was tracked down. The 30-minute cron called refreshAllUsers(), which iterated users; for one specific user whose Google refresh_token had been revoked (invalid_grant), the throw above fired inside getAllGoogleAccounts()'s try/catch. The whole node process died with no JS trace — the outer caller's try/catch in refreshAllUsers() (also around an await) never saw the error either.
Out log:
[bg-refresh] Refreshing user 2/10: 7ba959e7-...
[bg-refresh] Starting refresh for user 7ba959e7-...
<— process exits here, no error from any catch block —>
[cron] schedulers registered: ...
Server listening on http://0.0.0.0:3004
Error log alongside it was full of [PERRY WARN] js_box_get: invalid box pointer 0x... and [WARN_NULL_PTR] js_object_set_field: null POINTER_TAG ... — replacing with undefined, but no thrown error.
Workaround that resolved the production crash
Stop crossing throw with await + try/catch. Convert error-signaling to a result tag, so the await boundary never sees an in-flight exception:
interfaceResult<T>{ok: boolean;value: T|null;error: string;status: number;}asyncfunctionrefreshToken(refresh: string): Promise<Result<RefreshResp>>{constr=awaitfetch(URL, ...);conststatus=r ? r.status : 0;if(!r.ok){return{ok: false,value: null,error: "refresh failed: "+status, status };}return{ok: true,value: JSON.parse(awaitr.text()),error: "", status };}asyncfunctiongetAll(userId: string): Promise<...>{constresult=awaitrefreshToken(account.refresh_token);if(!result.ok||result.value===null){console.error("refresh failed:",result.error);returnnull;}// use result.value}
After this change, the cron completed all users instead of crashing on the second one.
Suspected cause
The codebase under question (gscmaster-api) carries inline comments that already note this — e.g. src/middleware/auth.ts:
// Perry: no try/catch (setjmp/longjmp incompatible with async context).// On error, js_jwt_verify returns null → js_json_parse(null) → JSValue::null() → payload falsy.
This is consistent with #856 (`_setjmp` redeclared with mismatched signatures in gc.rs vs promise.rs, potential UB). If the setjmp buffer is being interpreted as the wrong type at one of those two call sites, an await-boundary unwind may stomp on it and corrupt subsequent state, eventually faulting silently.
Either way, detect and refuse to compilethrow patterns where the catch is across an await boundary, OR emit a clear runtime error instead of silently exiting. Right now this pattern looks like normal JS to anyone who writes it, and the failure mode (week-long PM2 restart loop with no JS log) is genuinely scary.
A doc note in the language guide ("Don't throw from async + try/catch around await — use result tags") would have saved a week of production debugging.
Environment
Perry: git rev-parse HEAD → 32352733 (fix(codegen): #678 — re-export rename resolves to origin export name (#785))
Target: x86_64 Ubuntu 22.04
Runtime: Fastify (Perry's bundled shim) + mysql2
Filed from gscmaster-api investigation; full conversation thread available on request.
Symptom
In a binary compiled with Perry (commit
32352733), the process exits silently — no JS error message, no stack trace, just a clean exit code that PM2 (or any supervisor) treats as a crash and restarts. The trigger is a pattern that should be a basic try/catch:Expected: the
throwis caught by thecatchblock,console.errorruns.Actual: the process dies. No log line from
catch. The supervisor restarts the binary.Real-world manifestation
A production service (
gscmaster-api, Fastify+Perry) crashed every 30 minutes for 7 days (330 PM2 restarts) before the root cause was tracked down. The 30-minute cron calledrefreshAllUsers(), which iterated users; for one specific user whose Googlerefresh_tokenhad been revoked (invalid_grant), thethrowabove fired insidegetAllGoogleAccounts()'stry/catch. The whole node process died with no JS trace — the outer caller's try/catch inrefreshAllUsers()(also around anawait) never saw the error either.Out log:
Error log alongside it was full of
[PERRY WARN] js_box_get: invalid box pointer 0x...and[WARN_NULL_PTR] js_object_set_field: null POINTER_TAG ... — replacing with undefined, but no thrown error.Workaround that resolved the production crash
Stop crossing
throwwithawait+try/catch. Convert error-signaling to a result tag, so theawaitboundary never sees an in-flight exception:After this change, the cron completed all users instead of crashing on the second one.
Suspected cause
The codebase under question (
gscmaster-api) carries inline comments that already note this — e.g.src/middleware/auth.ts:This is consistent with #856 (`_setjmp` redeclared with mismatched signatures in
gc.rsvspromise.rs, potential UB). If thesetjmpbuffer is being interpreted as the wrong type at one of those two call sites, anawait-boundary unwind may stomp on it and corrupt subsequent state, eventually faulting silently.Asks
throwpatterns where the catch is across anawaitboundary, OR emit a clear runtime error instead of silently exiting. Right now this pattern looks like normal JS to anyone who writes it, and the failure mode (week-long PM2 restart loop with no JS log) is genuinely scary.throwfrom async + try/catch around await — use result tags") would have saved a week of production debugging.Environment
git rev-parse HEAD→32352733(fix(codegen): #678 — re-export rename resolves to origin export name (#785))Filed from gscmaster-api investigation; full conversation thread available on request.