dd-trace-js icon indicating copy to clipboard operation
dd-trace-js copied to clipboard

[BUG]: mysql2 instrumentation not supporting promises

Open alexpavlov opened this issue 2 weeks ago • 2 comments

Tracer Version(s)

5.80.0

Node.js Version(s)

22.11.0

Bug Report

When using mysql2/promise with dd-trace, the mysql2 instrumentation can return a callback-style Query object instead of a Promise when the appsec abortController.signal.aborted check is true. This causes mysql2's Query.then() safety method to throw an error.

The issue is in packages/datadog-instrumentations/src/mysql2.js lines 167-204. When wrapping Pool.prototype.query, if the abort signal is triggered, the code returns queryCommand (a callback-style object) instead of a Promise:

shimmer.wrap(Pool.prototype, 'query', query => function (sql, values, cb) {
  // ...
  if (abortController.signal.aborted) {
    // ... 
    return queryCommand;  // This is NOT a promise!
  }
  return query.apply(this, arguments)
})

When application code awaits this result, mysql2's Query class has a then() method that throws: You have tried to call .then(), .catch(), or invoked await on the result of query that is not a promise...

Reproduction Code

  1. Use Next.js 15+ with mysql2/promise
  2. Enable dd-trace with default settings (appsec enabled)
  3. On first request after server restart, when a connection pool is being created, the error appears
// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    const tracer = await import('dd-trace');
    tracer.default.init({
      runtimeMetrics: true,
    });
  }
}

// lib/db.ts
import mysql from 'mysql2/promise';

const pool = mysql.createPool({ /* config */ });
const [rows] = await pool.query('SELECT * FROM table'); // Throws error

Error Logs

DEBUG: Creating pool for: [schema_id]
You have tried to call .then(), .catch(), or invoked await on the result of query that is not a promise, which is a programming error. Try calling con.promise().query(), or require('mysql2/promise') instead of 'mysql2' for a promise-compatible version of the query interface.
Error: aborted
    at ignore-listed frames {
  code: 'ECONNRESET'
}

Workaround

Disabling appsec resolves the issue:

tracer.default.init({
  appsec: { enabled: false },
});

Suggested Fix

The wrapPool function should properly handle the promise wrapper. When the pool is from mysql2/promise, the wrapped query should return a rejected Promise instead of a callback-style object:

if (abortController.signal.aborted) {
  // Return a rejected promise for promise-based pools
  if (this.promise) {
    return Promise.reject(abortController.signal.reason);
  }
  // ... existing callback handling
}

Operating System

macOS Darwin 24.6.0 (also reproducible on Linux)

Additional Context

  • mysql2 version: 3.15.3

Reproduction Environment

The error is triggered when a database connection is severed (ECONNRESET). This is easier to reproduce in the Next.js development environment where:

  1. Duplicate requests are common due to React Server Components behavior in dev mode (React's strict mode double-invokes components)
  2. Long compilation times on first request can cause client-side request timeouts/aborts
  3. Hot module replacement can interrupt in-flight database connections

Error Logs

No response

Tracer Config

No response

Operating System

No response

Bundling

Unsure

alexpavlov avatar Dec 07 '25 15:12 alexpavlov

Thank you for reporting this in details. I'm part of the team that own this part of the code, and I will investigate it myself. As we're approaching holiday season, this might take more time than it should. But feel free to ping me again if too much time has passed.

simon-id avatar Dec 08 '25 21:12 simon-id

I'm also taking the liberty to rename this issue, as it looks like it has no title ? Feel free to rename it yourself better if you want to.

simon-id avatar Dec 08 '25 21:12 simon-id