Skip to content

fix(contract): stop the handler errors map from being thenable - #2113

Merged
dinwwwh merged 1 commit into
middleapi:mainfrom
dinwwwh:claude/error-handler-thenable-hang-6af435
Sep 28, 2026
Merged

dinwwwh merged 1 commit into
middleapi:mainfrom
dinwwwh:claude/error-handler-thenable-hang-6af435

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 28, 2026

Copy link
Copy Markdown
Member

Returning errors from an async handler (or awaiting it anywhere) no longer hangs forever. The errors map returned a constructor for every string key, including then, so promise resolution treated it as a thenable that never settles. It now unwraps the same keys as createORPCClient.

Fixes

  • os.handler(async ({ errors }) => errors) and await errors settle instead of hanging.
  • String(errors) no longer throws, and JSON.stringify(errors) no longer serializes a made-up toJSON error.
  • Wire errors with codes like toString are unaffected; reconciliation still looks codes up on the raw error map.

Behavior change

errors.then(), errors.toString(), errors.valueOf(), errors.toJSON() (and bind/call/apply) no longer build errors with those codes; use new ORPCError('toString') instead.

Testing

  • New regression test for symbol and unwrap keys fails without the change.
  • Contract and server tests (687), lint, and @orpc/contract type-check pass.

The errors map passed to handlers returned a constructor for every
string key, including `then`, so awaiting it or returning it from an
async function never settled. It now unwraps the same keys as
createORPCClient (then, toString, valueOf, toJSON, ...) instead of
treating them as error codes.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

Fixes an infinite hang when the handler errors constructor map is awaited or returned from an async handler. The proxy returned a constructor for every string key including then, making it a never-settling thenable; promise resolution against it blocked forever. One commit, two files.

  • error-factory.ts get trap — unwrap keys now fall through to Reflect.get(target, code) instead of fabricating a constructor, reusing the shared RECURSIVE_CLIENT_UNWRAP_KEYS from @orpc/client. This is the exact shape of the existing client.ts, client-safe.ts, and router-client.ts proxies, so the behavior is consistent across recursive proxies.
  • Regression test — symbol access and unwrap-key handling are merged into one test that asserts then/toJSON are undefined, toString/valueOf resolve to Object.prototype methods, and await map resolves to the proxy; the former toString-code assertion moves to constructor. It fails without the fix (map.then would be a function; the await would hang).
  • Behavior change — codes named then, bind, call, apply, valueOf, toString, toJSON can no longer be invoked as constructors on the map. That is the intended trade-off for the fix and mirrors the reserved-key restriction already documented for routers; reconciliation still uses getOwn on the raw map, so wire-level matching is unaffected.

Tests pass locally: packages/contract/src/error-factory.test.ts (24 passed).

Pullfrog  | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@pkg-pr-new

pkg-pr-new Bot commented Sep 28, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@2113

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@2113

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@2113

@orpc/client

npm i https://pkg.pr.new/@orpc/client@2113

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@2113

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@2113

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@2113

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@2113

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@2113

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@2113

@orpc/experimental-lock

npm i https://pkg.pr.new/@orpc/experimental-lock@2113

@orpc/experimental-msw

npm i https://pkg.pr.new/@orpc/experimental-msw@2113

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@2113

@orpc/next

npm i https://pkg.pr.new/@orpc/next@2113

@orpc/node

npm i https://pkg.pr.new/@orpc/node@2113

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@2113

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@2113

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@2113

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@2113

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@2113

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@2113

@orpc/server

npm i https://pkg.pr.new/@orpc/server@2113

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@2113

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@2113

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@2113

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@2113

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@2113

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@2113

commit: a451bfa

@codecov

codecov Bot commented Sep 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will degrade performance by 12.56%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

❌ 1 regressed benchmark
✅ 29 untouched benchmarks

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ octet stream 640.5 µs 732.6 µs -12.56%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing dinwwwh:claude/error-handler-thenable-hang-6af435 (a451bfa) with main (eab53d6)

Open in CodSpeed

@dinwwwh
dinwwwh merged commit 94f55fd into middleapi:main Sep 28, 2026
10 of 11 checks passed
dinwwwh added a commit that referenced this pull request Sep 30, 2026
The handler `errors` map now reserves only `then`, instead of every key
in `RECURSIVE_CLIENT_UNWRAP_KEYS` as #2113 did. `then` is the only key
the runtime reads on its own (promise resolution), so it is the only one
that caused a real failure. The others are only reached by explicitly
converting or serializing the map. This narrows the behavior change from
#2113.

## Fixes

- `errors.toString()`, `errors.valueOf()`, `errors.toJSON()`,
`errors.bind()`, `errors.call()` and `errors.apply()` build errors with
those codes again.
- `await errors` and returning `errors` from an async handler still
settle instead of hanging.
- Adding a key to the client's unwrap set no longer silently takes that
name away from error codes.

## Testing

- The regression test now checks only symbols and `then`. The
Object.prototype lookup test uses `toString()` again and fails if
`toString` is reserved.
- Contract and server tests (687), eslint, and the `@orpc/contract`
type-check pass.
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.

1 participant