2
\$\begingroup\$

I've been using the excellent pg-promise library for a few years now, but my primary irk with it is that the stack traces are sometimes unhelpful. For example, the following test will fail (currently on nodejs v16.x):

  const pgdb = pgp(config) // see pg-promise link for details, but just know that various db call methods are exposed as below:

  it('should print a stack trace that includes all calling functions from a db call', async () => {
    async function one() {
      await two()
    }

    async function two() {
      await three()
    }
    async function three() {
      await pgdb.result('SELECT * FROM does_not_exist')
    }

    try {
      await one()
      assert.fail(`call to function 'one' should have thrown an error`)
    } catch (err) {
      const stack = err.stack
      assert.include(stack, 'three', `could not find call to function 'three' in stack`)
      assert.include(stack, 'two', `could not find call to function 'two' in stack`)
      assert.include(stack, 'one', `could not find call to function 'one' in stack`)
    }
)

This is because the stack comes out like this:

error: relation "does_not_exist" does not exist
    at Parser.parseErrorMessage (/app/node_modules/pg-protocol/dist/parser.js:287:98)
    at Parser.handlePacket (/app/node_modules/pg-protocol/dist/parser.js:126:29)
    at Parser.parse (/app/node_modules/pg-protocol/dist/parser.js:39:38)
    at Socket.<anonymous> (/app/node_modules/pg-protocol/dist/index.js:11:42)
    at Socket.emit (node:events:390:28)
    at addChunk (node:internal/streams/readable:315:12)
    at readableAddChunk (node:internal/streams/readable:289:9)
    at Socket.Readable.push (node:internal/streams/readable:228:10)
    at TCP.onStreamRead (node:internal/stream_base_commons:199:23)
    at TCP.callbackTrampoline (node:internal/async_hooks:130:17)

The stack trace has no context for where this was actually thrown. To fix this problem the author recommends using Bluebird promises for the better stack-trace capabilities, but recent advancements in node.js have rendered async/await better for performance.

I understand why this is happening, but it doesn't make my job easier. So, I thought I could us a Proxy to wrap the root database client object to get a proper stack trace:

// initializer module to do all the database client setup
const intermediate = pgp(_config)  // create an intermediate client

// create a new object; creating a proxy from `intermediate` will throw 
// `property 'result' is a read-only and non-configurable data property` errors 
// when accessed through a proxy 
const _pgdb = { ...intermediate } 

const handler = {
  get: function (target, prop) {
    if (prop in target) {
      const property = target[prop]
      if (typeof property === 'function') {
        return async function (...args) {
          try {
            return await property.apply(target, args)
          } catch (err) {
            throw new Error(`error calling database function '${prop}': ${err.message}`)
          }
        }
      }
      return property
    }
    // there are some properties like `$config` that aren't destructured from `intermediate`
    if (prop in intermediate) {
      return intermediate[prop]
    }

    return undefined
  }
}

const pgdb = new Proxy(_pgdb, handler)

Stack traces now look like:

Error: error calling database function 'result': relation "does_not_exist" does not exist
    at Proxy.<anonymous> (/app/db/initializer.js:109:19)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async three (/app/test/dbal/utils.test.js:252:7)
    at async two (/app/test/dbal/utils.test.js:249:7)
    at async one (/app/test/dbal/utils.test.js:245:7)
    at async Context.<anonymous> (/app/test/dbal/utils.test.js:256:7)

While this works and gives me the exact lines where the failure was thrown, it seems like a dirty hack and I feel like I'm missing something that could make this wrapper a little more elegant.

\$\endgroup\$
2
  • \$\begingroup\$ Of course the first one fails, you've got an unmatched parenthesis! \$\endgroup\$
    – emanresu A
    Mar 19, 2022 at 5:15
  • \$\begingroup\$ If that were true, that’s not the stacktrace I’d be getting :) apologies for the partial snippet \$\endgroup\$
    – BrDaHa
    Mar 19, 2022 at 7:08

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.