K
Kysely3mo ago
Pantheon

Roll back transaction prematurely

Given a Transaction, how do I roll it back without throwing an Error inside the execution callback method?
return this._db.transaction().execute(async trx => {
const entryType = await this._db
.selectFrom("updateLogEntryType")
.selectAll()
.where("kind", "=", props.updateKind)
.executeTakeFirst();

if (!entryType) {
logger.error("Unable to create update log with unknown type:", entryType);
// <-- rollback transaction here, without throwing an Error
return new Err("EntryTypeNotFound");
}
);
return this._db.transaction().execute(async trx => {
const entryType = await this._db
.selectFrom("updateLogEntryType")
.selectAll()
.where("kind", "=", props.updateKind)
.executeTakeFirst();

if (!entryType) {
logger.error("Unable to create update log with unknown type:", entryType);
// <-- rollback transaction here, without throwing an Error
return new Err("EntryTypeNotFound");
}
);
Solution:
I managed to solve it with a wrapper function. Here it is, for anyone who may need it in the future: ``ts /** * Wraps a Kysely transaction such that any Err` returned from the callback results in a rollback....
Jump to solution
4 Replies
koskimas
koskimas3mo ago
There's no other way to do that. Why would you want to continue the function after the transaction has ended? Oh, and you're using this._db inside the transaction. That's a horrible horrible idea. You need to use trx or the queries don't take part in the transaction. And if you run queries using other connections than the trx inside the transction, you're going to get deadlocks.
Pantheon
PantheonOP3mo ago
Yeah, that was a mistake left over from before starting the refactor
Pantheon
PantheonOP3mo ago
The reason for all this is I'm trying to make transactions play nice with the error handling pattern from https://github.com/JohannesKlauss/ts-results-es
GitHub
GitHub - JohannesKlauss/ts-results-es: A typescript implementation ...
A typescript implementation of Rust's Result object. - JohannesKlauss/ts-results-es
Solution
Pantheon
Pantheon3mo ago
I managed to solve it with a wrapper function. Here it is, for anyone who may need it in the future:
/**
* Wraps a Kysely transaction such that any `Err` returned from the callback results in a rollback.
* Any exceptions thrown will result in an `Err<"UnknownTransactionError">`.
* @param db The Kysely database instance.
* @param cb The transaction handler which returns a `Result`.
* @returns The Result from the transaction callback, or an `Err<"UnknownTransactionError">`.
*/
export default async function transactResult<V, E extends string>(
db: Kysely<Tables>,
cb: (trx: Transaction<Tables>) => Promise<Result<V, E>>
): Promise<Result<V, E | "UnknownTransactionError">> {
class InvokedRollback extends Error {
constructor(public err: Err<E>) {
super();
}
}

try {
const transactionOkValue = await db.transaction().execute(async trx => {
const returnValue = await cb(trx);
if (returnValue.isErr()) {
logger.error("Transaction handler returned an Err:", returnValue.error);

// make Kysely rollback this transaction
throw new InvokedRollback(returnValue);
}

return returnValue;
});

return transactionOkValue;
} catch (error) {
if (error instanceof InvokedRollback) {
logger.error("Transaction rolled back.");
return error.err as Err<E>;
} else {
// Another unknown error thrown inside the transaction handler.
logger.error(
"Unknown error thrown inside transaction handler. This may indicate a missing Err check:",
error
);
return new Err("UnknownTransactionError");
}
}
}
/**
* Wraps a Kysely transaction such that any `Err` returned from the callback results in a rollback.
* Any exceptions thrown will result in an `Err<"UnknownTransactionError">`.
* @param db The Kysely database instance.
* @param cb The transaction handler which returns a `Result`.
* @returns The Result from the transaction callback, or an `Err<"UnknownTransactionError">`.
*/
export default async function transactResult<V, E extends string>(
db: Kysely<Tables>,
cb: (trx: Transaction<Tables>) => Promise<Result<V, E>>
): Promise<Result<V, E | "UnknownTransactionError">> {
class InvokedRollback extends Error {
constructor(public err: Err<E>) {
super();
}
}

try {
const transactionOkValue = await db.transaction().execute(async trx => {
const returnValue = await cb(trx);
if (returnValue.isErr()) {
logger.error("Transaction handler returned an Err:", returnValue.error);

// make Kysely rollback this transaction
throw new InvokedRollback(returnValue);
}

return returnValue;
});

return transactionOkValue;
} catch (error) {
if (error instanceof InvokedRollback) {
logger.error("Transaction rolled back.");
return error.err as Err<E>;
} else {
// Another unknown error thrown inside the transaction handler.
logger.error(
"Unknown error thrown inside transaction handler. This may indicate a missing Err check:",
error
);
return new Err("UnknownTransactionError");
}
}
}
Want results from more Discord servers?
Add your server