Home > Software engineering >  Use transaction session in try/catch function from wrapper
Use transaction session in try/catch function from wrapper

Time:06-28

In multiple functions I'm running more than one database action. When one of these fails I want to revert the ran actions. Therefore I'm using a transaction session from Mongoose.

First I create a session with the startSession function. I've added the session to the different Model.create functions. At the end of the function I'm committing and ending the session.

Since I work with an asyncHandler wrapper on all my function I'm not retyping the try/catch pattern inside my function. Is there a way to get the session into the asyncHandler of a different wrapper to abort the transaction when one or more of these functions fail?

Register function example

import { startSession } from 'mongoose';
import Company from '../models/Company';
import Person from '../models/Person';
import User from '../models/User';
import Mandate from '../models/Mandate';
import asyncHandler from '../middleware/asyncHandler';

export const register = asyncHandler(async (req, res, next) => {    
    const session = await startSession();
    
    let entity;

    if(req.body.profile_type === 'company') {
        entity = await Company.create([{ ...req.body }], { session });
    } else {
        entity = await Person.create([{ ...req.body }], { session });
    }

    // Create user
    const user = await User.create([{ 
        entity,
        ...req.body
    }], { session });

    // Create mandate
    await Mandate.create([{
        entity,
        status: 'unsigned'
    }], { session });

    // Generate user account verification token
    const verification_token = user.generateVerificationToken();

    // Send verification mail
    await sendAccountVerificationMail(user.email, user.first_name, user.language, verification_token);

    await session.commitTransaction();
    session.endSession();

    res.json({
        message: 'User succesfully registered. Check your mailbox to verify your account and continue the onboarding.',
    })
});

asyncHandler helper

const asyncHandler = fn => ( req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

export default asyncHandler;

EDIT 1

Let me rephrase the question. I'm looking for a way (one or more wrapper functions or a different method) to avoid rewriting the lines with // ~ repetitive code behind it. A try/catch block and handling the start and abort function of a database transaction.

export const register = async (req, res, next) => {    
    const session = await startSession(); // ~ repetitive code
    session.startTransaction(); // ~ repetitive code

    try { // ~ repetitive code     
        let entity;

        if(req.body.profile_type === 'company') {
            entity = await Company.create([{ ...req.body }], { session });
        } else {
            entity = await Person.create([{ ...req.body }], { session });
        }

        const mandate = await Mandate.create([{ entity, status: 'unsigned' }], { session });

        const user = await User.create([{ entity, ...req.body }], { session });
        const verification_token = user.generateVerificationToken();
        
        await sendAccountVerificationMail(user.email, user.first_name, user.language, verification_token);

        await session.commitTransaction(); // ~ repetitive
        session.endSession(); // ~ repetitive

        res.json({
            message: 'User succesfully registered. Check your mailbox to verify your account and continue the onboarding.',
        });
    } catch(error) { // ~ repetitive
        session.abortTransaction(); // ~ repetitive
        next(error) // ~ repetitive
    } // ~ repetitive
};

CodePudding user response:

If you put the repetitive code in a class

class Transaction {
  async middleware(req, res, next) {
    const session = await startSession();
    session.startTransaction();
    try {
      await this.execute(req, session);
      await session.commitTransaction();
      session.endSession();
      this.message(res);
    } catch (error) {
      session.abortTransaction();
      next(error);
    }
  }
  async execute(req, session) { }
  message(res) { }
}

then you can inherit from that class to put in the non-repetitive parts:

class Register extends Transaction {
  async execute(req, session) {
    let entity;
    if (req.body.profile_type === 'company') {
      entity = await Company.create([{ ...req.body }], { session });
    } else {
      entity = await Person.create([{ ...req.body }], { session });
    }
    const mandate = await Mandate.create([{ entity, status: 'unsigned' }], { session });
    const user = await User.create([{ entity, ...req.body }], { session });
    const verification_token = user.generateVerificationToken();
    await sendAccountVerificationMail(user.email, user.first_name, user.language, verification_token);
  }
  message(res) {
    res.json({
      message: 'User succesfully registered. Check your mailbox to verify your account and continue the onboarding.',
    });
  }
}
export const register = async (req, res, next) => {
  new Register().middleware(req, res, next);
}

CodePudding user response:

I don't know where you got your asyncHandler logic, but it is very similar to what is used here and if it's not from there, I believe that article combined with this one about res.locals should answer your question.

By the way, the usage of express is assumed from your code and if I'm right, this question has way more to do with express than anything else and in that case I'd edit the tags to only include javascript and express.

Why I didn't I mark this as a duplicate though?

Well, after searching for answers I also bumped into Express 5 and I thought it would be interesting to mention that Starting with Express 5, route handlers and middleware that return a Promise will call next(value) automatically when they reject or throw an error

Which means that with Express 5, you can just do something like:

app.get('/user/:id', async (req, res, next) => {
  const user = await getUserById(req.params.id)
  res.send(user)
})

And any errors will be implicitly handled behind the scenes by Express, meaning that if await getUserById would somewhy fail, express would automatically call next for you, passing the flow to e.g. some error handler:

app.use((err, req, res, next) => {
    console.log(err);
});

Edit for OP's revision

This is a programming patterns issue. My opinion is that you should definitely explicitly write all of the try..catch, startSession and abortTransaction blocks inside every database function such as register like you have done.

What you could do instead is to implement shared error handling between all of these database functions.

There are multiple reasons for why I am suggesting this:

  • It is generally a bad idea to have very large try...catch blocks, which you will technically have, if all of the database functions are under the same try...catch. Large try...catch blocks make debugging harder and can result into unexpected situations. They will also prevent fine tuning of handling of exceptions, should the need arise (and it often will). Think of this as your program just saying "error", no matter what the error is; that's not good!

  • Don't use transactions if you don't need to, as they can introduce unnecessary performance overhead and if you just "blindly" wrap everything into a transaction, it could accidentally result into a database deadlock. If you really really want to, you could create some kind of utility function as shown below, as that too would at least scope / restrict the transaction to prevent the transaction logic "leaking"

Example:

// Commented out code is what you'd actually have

(async () => {
  
  const inTransaction = async (fn, params) => {
    //const session = await startSession();
    const session = "session";

    let result = await fn(session, ...params);

    //await session.commitTransaction();
    //session.endSession();

    return result;
  };
  
  let req = 0;
  
  console.log(req);

  
  const transactionResult = await inTransaction(async (session, req) => {
    //return Company.create([{ ...req.body }], { session });
    return new Promise(resolve => setTimeout(() => { resolve(req) }, 500));
  }, [10]);
  
  req  = transactionResult;

  console.log(req);
})();

So eventhough e.g. putting all code into one try...catch does prevent "duplicate code", the matter is not as black and white as "all duplicate code is bad!". Every so often when programming, you will stumble upon situations where it is a perfectly valid solution to repeat yourself and have some dreaded duplicate code (

  • Related