Skip to content

Transaction does not properly rollback when using Promise.all #1604

@mvelzel

Description

@mvelzel

This is related to this issue: #899, but that issue was marked invalid and closed.

When using an await Promise.all construction inside a transaction, an error that is thrown inside the one of the promises causes the await Promise.all to fail, but the transaction does not properly get rolled back.

I managed to create a reproducible example in the form of a unit test here:

describe("Transactions", () => {
  beforeEach(async () => {
    await db.schema
      .createTable("transactions_promises_tests")
      .addColumn("name", "text")
      .execute();
  });

  afterEach(async () => {
    await db.schema.dropTable("transactions_promises_tests").execute();
  });

  it("rollback when a Promise inside Promise.all fails", async () => {
    const names = ["test1", "test2", "error"];

    const transaction = db.transaction().execute(async (trx) => {
      await Promise.all(
        names.map(async (name) => {
          if (name === "error") throw new Error("Test error");
          await trx
            .insertInto("transactions_promises_tests")
            .values([{ name }])
            .execute();
        }),
      );
    });

    await expect(transaction).rejects.toThrow("Test error");

    const tableContents = await db
      .selectFrom("transactions_promises_tests")
      .selectAll()
      .execute();

    // This fails
    expect(tableContents.length).toBe(0);
  });

  it("rollback when a Promise inside a for-loop containing 'await [Promise]' fails", async () => {
    const names = ["test1", "test2", "error"];

    const transaction = db.transaction().execute(async (trx) => {
      for (const name of names) {
        if (name === "error") throw new Error("Test error");
        await trx
          .insertInto("transactions_promises_tests")
          .values([{ name }])
          .execute();
      }
    });

    await expect(transaction).rejects.toThrow("Test error");

    const tableContents = await db
      .selectFrom("transactions_promises_tests")
      .selectAll()
      .execute();

    // This succeeds
    expect(tableContents.length).toBe(0);
  });
});

I first noticed this problem when using a Promise.all construction inside a migration. Then I discovered the problem applies to transactions in general. My current workaround is to rewrite all my Promise.all logic to the second construction shown in the above example, which is just a simple for (...) { await (...) } construction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    invalidThis doesn't seem right

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions