Skip to content

perf: defer Error object creation to error handlers in promise wrappers - #4257

Merged
sidorares merged 1 commit into
masterfrom
perf/defer-error-creation
Apr 10, 2026
Merged

sidorares merged 1 commit into
masterfrom
perf/defer-error-creation

Conversation

@sidorares

Copy link
Copy Markdown
Owner

Summary

  • Moves new Error() instantiation from function scope into error handling blocks across all promise wrapper modules, so Error objects are only created when actual errors occur
  • Eliminates unnecessary CPU and GC overhead from stack trace capture on every successful call in high-throughput scenarios
  • Updates test-async-stack to verify error propagation without asserting on caller-frame line numbers (which are no longer captured at the call site)

Trade-off

The previous pattern captured a stack trace at the call site on every invocation so that, on error, the rejected Error would include the caller's frame. This is useful for debugging but expensive: in benchmarks over 280k calls the eager new Error() was a measurable CPU hot-spot. After this change, stack traces on errors still exist but originate from the internal callback rather than the caller.

Changes

File Change
lib/promise/make_done_cb.js Remove localErr parameter; create Error inside if (err)
lib/promise/connection.js Remove eager new Error() from all 10 methods
lib/promise/pool.js Same for query, execute, end
lib/promise/pool_cluster.js Same for query, execute
lib/promise/prepared_statement_info.js Same for execute
promise.js Same for createConnectionPromise, PromisePoolCluster methods
test/…/test-async-stack.test.mts Assert error propagation (code, message, stack presence) instead of caller-frame line offsets

Continues #3169 — rebased onto the current modular codebase.

Co-authored-by: gunman gunman@neowiz.com

Test plan

  • npm run lint — passes
  • act CI Build (typecheck + circular imports) — passes
  • act CI Linux (Node 22, mysql:8.3, SSL=0, Compression=0) — 205/205 tests pass

@codecov

codecov Bot commented Apr 10, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 45.45455% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.86%. Comparing base (0b750e0) to head (a1711f7).
⚠️ Report is 3 commits behind head on master.

Files with missing lines Patch % Lines
lib/promise/connection.js 20.00% 8 Missing ⚠️
promise.js 25.00% 3 Missing ⚠️
lib/promise/pool.js 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4257      +/-   ##
==========================================
- Coverage   90.87%   90.86%   -0.01%     
==========================================
  Files          89       89              
  Lines       14498    14487      -11     
  Branches     1862     1862              
==========================================
- Hits        13175    13164      -11     
  Misses       1323     1323              
Flag Coverage Δ
compression-0 90.13% <45.45%> (-0.01%) ⬇️
compression-1 90.84% <45.45%> (-0.01%) ⬇️
static-parser-0 88.58% <45.45%> (-0.01%) ⬇️
static-parser-1 89.33% <45.45%> (-0.01%) ⬇️
tls-0 90.30% <45.45%> (-0.01%) ⬇️
tls-1 90.64% <45.45%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Move `new Error()` instantiation from the call site into error handling
blocks so Error objects are only created when actual errors occur. In
high-throughput scenarios this eliminates unnecessary CPU and GC overhead
from stack trace capture on every successful call.

Continues work from #3169, rebased onto the current modular codebase.

Co-authored-by: gunman <gunman@neowiz.com>
@sidorares
sidorares force-pushed the perf/defer-error-creation branch from c10be68 to a1711f7 Compare April 10, 2026 13:50
@sidorares
sidorares merged commit ab131de into master Apr 10, 2026
88 checks passed
@sidorares
sidorares deleted the perf/defer-error-creation branch April 10, 2026 13:58
@chrisveness

Copy link
Copy Markdown
Contributor

Would it be possible to communicate the caller (application) filename / line-number where the error was triggered?

It has become much harder to debug MySQL errors without knowing where in the application code they originated.

@sidorares

sidorares commented Apr 15, 2026 •

Copy link
Copy Markdown
Owner Author

Would it be possible to communicate the caller (application) filename / line-number where the error was triggered?

It has become much harder to debug MySQL errors without knowing where in the application code they originated.

This was the original intent of manual error construction, and this PR should not change this mostly

Could you help testing this @chrisveness ? Create a simple repro case, and compare stack trace before / after this version

Also maybe we need to reevaluate if we still have to do this manually - v8 captures async stack traces manually, maybe we can rely on that

@chrisveness

Copy link
Copy Markdown
Contributor

Create file bad-query.js:

import mysql from 'mysql2/promise';

const connection = await mysql.createConnection({
  host:     'localhost',
  user:     'user',
  database: 'db',
  password: 'pw',
});

const sql = 'SELECT nosuchcolumn FROM information_schema.tables';
const [ results, fields ] = await connection.query(sql);
console.log(results);
await connection.end();

Run:

$ npm install mysql2@3.21.1
$ node bad-query.js

Gives:

.../mysql2-4257/node_modules/mysql2/lib/promise/connection.js:29
    const localErr = new Error();
                     ^

Error: Unknown column 'nosuchcolumn' in 'field list'
    at PromiseConnection.query (.../mysql2-4257/node_modules/mysql2/lib/promise/connection.js:29:22)
    at file://.../mysql2-4257/bad-query.js:11:46
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5) {
  code: 'ER_BAD_FIELD_ERROR',
  errno: 1054,
  sql: 'SELECT nosuchcolumn FROM information_schema.tables',
  sqlState: '42S22',
  sqlMessage: "Unknown column 'nosuchcolumn' in 'field list'"
}

Then run:

$ npm install mysql2@3.22.0
$ node bad-query.js

Gives:

.../mysql2-4257/node_modules/mysql2/lib/promise/make_done_cb.js:6
      const localErr = new Error();
                       ^

Error: Unknown column 'nosuchcolumn' in 'field list'
    at Query.onResult (.../mysql2-4257/node_modules/mysql2/lib/promise/make_done_cb.js:6:24)
    at Query.execute (.../mysql2-4257/node_modules/mysql2/lib/commands/command.js:36:14)
    at Connection.handlePacket (.../mysql2-4257/node_modules/mysql2/lib/base/connection.js:552:34)
    at PacketParser.onPacket (.../mysql2-4257/node_modules/mysql2/lib/base/connection.js:102:12)
    at PacketParser.executeStart (.../mysql2-4257/node_modules/mysql2/lib/packet_parser.js:75:16)
    at Socket.<anonymous> (.../mysql2-4257/node_modules/mysql2/lib/base/connection.js:109:25)
    at Socket.emit (node:events:508:28)
    at addChunk (node:internal/streams/readable:559:12)
    at readableAddChunkPushByteMode (node:internal/streams/readable:510:3)
    at Readable.push (node:internal/streams/readable:390:5) {
  code: 'ER_BAD_FIELD_ERROR',
  errno: 1054,
  sql: 'SELECT nosuchcolumn FROM information_schema.tables',
  sqlState: '42S22',
  sqlMessage: "Unknown column 'nosuchcolumn' in 'field list'"
}

... which has no reference to bad-query.js line number.

(Sometimes it might be straightforward to grep for the SQL string, but not always).

@sidorares

Copy link
Copy Markdown
Owner Author

yeah, I should have reviewed this a bit more carefully
when we moved stack capture inside callback it broke original funcionality of capturing command stack

I'll see current state of native async stack traces but might actually revert this change if there's no better option

We could have this optional with the flag, but I'm not sure how big of a perf impact are we talking realistically compared to a full round trip of an average query. If it's really small flag not worth it and I'll just revert

@sidorares

sidorares commented Apr 16, 2026 •

Copy link
Copy Markdown
Owner Author

I think it's best to revert.

Alternatives considered:

'lightweight stacks' -

  const obj = {};
  Error.captureStackTrace(obj, captureCallSite);

Same performance as new Error() in node, in Bun new Error() is actually faster compared to Error.captureStackTrace

native async stack traces - only captures await boundary.

@sidorares

Copy link
Copy Markdown
Owner Author

@chrisveness reverted, releasing as v3.22.1

mdierolf pushed a commit to CloudQuote/node-mysql2 that referenced this pull request May 23, 2026
…rs (sidorares#4257)

Move `new Error()` instantiation from the call site into error handling
blocks so Error objects are only created when actual errors occur. In
high-throughput scenarios this eliminates unnecessary CPU and GC overhead
from stack trace capture on every successful call.

Continues work from sidorares#3169, rebased onto the current modular codebase.

Co-authored-by: gunman <gunman@neowiz.com>
mdierolf pushed a commit to CloudQuote/node-mysql2 that referenced this pull request May 23, 2026
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.

2 participants