sequelize: Sequelize errors don't print properly in jest

Issue Creation Checklist

  • I understand that my issue will be automatically closed if I don’t fill in the requested information
  • I have read the contribution guidelines

Bug Description

When using sequelize in jest, errors don’t print their message, which makes debugging difficult. You get a trace back into the query interface in sequelize, but you have to set breakpoints in sequelize in order to determine the nature of the error (syntax error, foreign key constraint issue, etc.).

I have verified that other thrown errors print properly, so it seems to be an issue with these in particular in Sequelize.

Reproducible Example

it("passes", async () => {
  await db.query("SELECT 1 )").
});

In this example, let’s assume I want to write a test that selects 1 and passes. Assuming db is a Sequelize instance pointing at a postgres db, this test that we want to be successful fails due to a syntax error. In the console, I get a trace, but the error message is missing.

What do you expect to happen?

The error message prints indicating the sql syntax error. The full printout should look someting like:

DatabaseError: syntax error at or near ")"

      at Query.run (../node_modules/sequelize/src/dialects/postgres/query.js:76:25)
      at ../node_modules/sequelize/src/sequelize.js:641:28
      at PostgresQueryInterface.select (../node_modules/sequelize/src/dialects/abstract/query-interface.js:1001:12)
      at … remaining trace back to test

What is actually happening?

The error is printed, but only the stack trace appears, which will look something like this (note that message is omitted):

      at Query.run (../node_modules/sequelize/src/dialects/postgres/query.js:76:25)
      at ../node_modules/sequelize/src/sequelize.js:641:28
      at PostgresQueryInterface.select (../node_modules/sequelize/src/dialects/abstract/query-interface.js:1001:12)
      at … remaining trace back to test

Environment

  • Sequelize version: 6.21.3
  • Node.js version: 16.15.0
  • If TypeScript related: TypeScript version:
  • Database & Version: Postgres 11.x
  • Connector library & Version: pg@8.7.3

Would you be willing to resolve this issue by submitting a Pull Request?

  • Yes, I have the time and I know how to start.
  • Yes, I have the time but I will need guidance.
  • No, I don’t have the time, but my company or I are supporting Sequelize through donations on OpenCollective.
  • No, I don’t have the time, and I understand that I will need to wait until someone from the community or maintainers is interested in resolving my issue.

Indicate your interest in the resolution of this issue by adding the 👍 reaction. Comments such as “+1” will be removed.

About this issue

  • Original URL
  • State: open
  • Created 2 years ago
  • Reactions: 35
  • Comments: 28 (9 by maintainers)

Commits related to this issue

Most upvoted comments

Just checking in again to ask about the status of backporting this change to v6. Thanks!

Reopening until the fix has been back ported to v6

Looking forward for the back port. Will improve dev experience. 👍

For added safety I tweaked this a bit to run

// api/src/db/utils/monkey-patch-sequelize-errors-for-jest.ts
import { Sequelize } from "sequelize"

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sequelizeVersion = (Sequelize as any).version
const major = sequelizeVersion.split(".").map(Number)[0]

if (major >= 7) {
  console.warn("This patch was probably made redundant in Sequelize v7, you should check!")
}

/**
 * Fixed in Sequelize v7, but hasn't been back-ported to Sequelize v6.
 * See https://github.com/sequelize/sequelize/issues/14807#issuecomment-1854398131
 */
export function monkeyPatchSequelizeErrorsForJest(instance: Sequelize) {
  if (typeof jest === "undefined") return instance

  const origQueryFunc = instance.query
  instance.query = async function query(this: Sequelize, ...args: any[]) {
    let result
    try {
      result = await origQueryFunc.apply(this, args as any)
    } catch (err: any) {
      console.error(err) // Important - this is how we debug the error
      throw err
    }
    return result
  } as typeof origQueryFunc

  return instance
}

Usage

// api/src/db/db-client.ts
import { Sequelize, Options } from "sequelize"
import { monkeyPatchSequelizeErrorsForJest } from "@/db/utils/monkey-patch-sequelize-errors-for-jest"

export const SEQUELIZE_CONFIG: Options = {
  // ... your config
}

let db: Sequelize
if (NODE_ENV === "test") {
  db = monkeyPatchSequelizeErrorsForJest(new Sequelize(SEQUELIZE_CONFIG))
} else {
  db = new Sequelize(SEQUELIZE_CONFIG)
}

export default db

@ak-beam catching error and logging it using console.log is a simple but noisy workaround.

I created this error wrapper which shows correct error message, SQL statement, parameters and displays correct trace:

class ImprovedSequelizeError extends Error {
  constructor(originalError: SequelizeError) {
    super();
    this.name = originalError.name;

    let { message } = originalError.parent;
    if (originalError.sql) {
      message += "\nSQL: " + originalError.sql;
    }

    if (originalError.parameters) {
      const stringifiedParameters = JSON.stringify(originalError.parameters);
      if (
        stringifiedParameters !== "undefined" &&
        stringifiedParameters !== "{}"
      ) {
        message += "\nParameters: " + stringifiedParameters;
      }
    }

    message += "\n" + originalError.stack;

    this.message = message;

    Error.captureStackTrace(this, fixSequelizeError);
  }
}

const isSequelizeError = (e: unknown): e is SequelizeError =>
  e instanceof Error && e.name.startsWith("Sequelize");

const fixSequelizeError = (e: unknown) => {
  if (isSequelizeError(e)) {
    throw new ImprovedSequelizeError(e);
  }

  throw e;
};

We use fixSequelizeError instead of console.log in the tests.

try {
  // test code
} catch (e) {
  fixSequelizeError(e);
}

We should fix this issue in all environments, it’s a problem in mocha and other tools that only print the .stack property

Following #13347, the stack went from

SequelizeDatabaseError: Invalid column name 'unknown_column'.
    at Query.formatError (/Users/harry/code/sequelize/lib/dialects/mssql/query.js:314:12)
    at Query._run (/Users/harry/code/sequelize/lib/dialects/mssql/query.js:93:18)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)

To

Error
    at Query.run (/Users/harry/code/sequelize/lib/dialects/mssql/query.js:119:25)
    at /Users/harry/code/sequelize/lib/sequelize.js:619:28
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at async Context.<anonymous> (/Users/harry/code/sequelize/test/integration/sequelize/query.test.js:319:11)

But it should have become this instead:

SequelizeDatabaseError: Invalid column name 'unknown_column'.
    at Query.run (/Users/harry/code/sequelize/lib/dialects/mssql/query.js:119:25)
    at /Users/harry/code/sequelize/lib/sequelize.js:619:28
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at async Context.<anonymous> (/Users/harry/code/sequelize/test/integration/sequelize/query.test.js:319:11)

It should merge both stacks (keeping the error message but replacing the trace) instead of fully replacing it.

I’ve started to look into this and you can use the parent or original property in v6 to retrieve the message you need (together with additional information). In v7 we are using the native Error#cause (or the cause property in versions before Node 16.19).

By the way, this issue is similar to #13978

Thanks for the workaround solutions! I combined the ImprovedSequelizeError class from @Naktibalda and the sequelize money-patcher from @ak-beam and @klondikemarlen to create a monkey-patch that actually just throws a (sorta) correct error instance: https://gist.github.com/mltsy/629d2b5b703f14e3e97eb8396518cfad

(The stack trace has a few more frames than it normally should, but it’s usable!)

Based on a comment above, this works to at least print the error (although Jest still prints the truncated version later).

Advantage of this is you can put it in the source code where the Sequelize instance is defined rather than changing all your tests.

function monkeyPatchSequelizeErrorsForJest(instance: Sequelize) {
  if (typeof jest === 'undefined') {
    return instance
  }
  const origQueryFunc = instance.query
  instance.query = async function query(this: Sequelize, ...args: any[]) {
    let result
    try {
      result = await origQueryFunc.apply(this, args as any)
    } catch (err: any) {
      console.error(err) // Important - this is how we debug the error
      throw err
    }
    return result
  } as typeof origQueryFunc
  return instance
}

export const db = monkeyPatchSequelizeErrorsForJest(new Sequelize(databaseUrl))

We’re working around this now using this function:

function monkeyPatchSequelizeErrorsForJest(instance: Sequelize) {
  if (typeof jest === "undefined") {
    return;
  }
  const origQueryFunc = instance.query;
  instance.query = function (this: Sequelize, ...args: any[]) {
    return origQueryFunc.apply(this, args as any).catch(async (err) => {
      if (typeof err.stack === "string") {
        const stackLines = err.stack.split("\n");
        if (stackLines[0] === "Error: ") {
          err.stack = `${stackLines[0]}${err.message}\n${stackLines.slice(1).join("\n")}`;
        }
      }
      throw err;
    }) as any; // Cast to any to hide that it's a promise. Otherwise jest will show this code as the root cause of the error
  } as typeof origQueryFunc;
}

Couldn’t the stack-trace be captured using this function and pass in the method to exclude (formatError): https://nodejs.org/api/errors.html#errorcapturestacktracetargetobject-constructoropt

Ah yes, this was already mentioned in #14408 This was introduced in #13347 and I think it’s a good change but we just need to see if we can return the original error (because of the name) with the injected stack trace.