Skip to content

Firebase how to break realtime database transaction with different state/message if different condition is true?

is it a good practice at all and if yes what is the correct way to break a transaction with different error states/messages for different situations?

I have a transaction running over an ‘offer’ entry doing ‘seats’ reservation:

I want to break it if one of the following 3 conditions is true and return state/message to the caller function.

  1. if the asking user already made a reservation of seats of this offer.
  2. if there is not enought seats.
  3. if this offer does not exists.

And if everything is ok the transaction should complete normally and return state/message to the caller function that reservation is made.

I’m not sure how to break the transaction in case of one of the conditions is true.

  • if I use throw new Error(‘description of the problem.’) then this will be an Exception and it’s not handled by catch() handler of the transaction Promise and I’m not sure how to handle this exception because it’s an asynchronous function here. so I think i should not use an exception.

Here is what I mean:

dealSeats = function(entryRef, data) {
    const TAG = '[dealSeats]: ';
    return entryRef.transaction((entry)=>{
        if (entry) {
            if ((entry.deals) && (entry.deals[data.uid])) {
                **? how to break the transaction with state/message 'You already have a deal.' ? and how to handle it below ?**
            } else if (entry.details.seatsCount >= data.details.seatsCount) {
                entry.details.seatsCount -= data.details.seatsCount;
                var deal = [];
                deal.status = 'asked';
                deal.details = data.details;
                if (!entry.deals) {
                    entry.deals = {};
                }
                entry.deals[data.uid] = deal;
            } else {
                **? how to break the transaction with state/message 'Not enought seats.' ? and how to handle it below ?**
            }
        }
        return entry;
        **? how to check if 'entry' is really null ? i.e. offer does not exists ?** and break and handle it.
    })
    .then((success)=>{
        return success.snapshot.val();
    })
    .catch((error)=>{
        return Promise.reject(error);
    });
}

here is my data in realtime database:

activeOffers
 -LKohyZ58cnzn0vCnt9p
    details
        direction: "city"
        seatsCount: 2
        timeToGo: 5
    uid: "-ABSIFJ0vCnt9p8387a"    ---- offering user

here is my test data sent by Postman:

{
 "data": 
  {
     "uid": "-FGKKSDFGK12387sddd",    ---- the requesting/asking user
     "id": "-LKpCACQlL25XTWJ0OV_",
     "details":
     {
         "direction": "city",
         "seatsCount": 1,
         "timeToGo": 5
     }
  }
}

==== updated with final source ====

many thanks to Renaud Tarnec!

So here is my final source that is working fine. If someone sees a potential problem please let me know. Thanks.

dealSeats = function(entryRef, data) {
    const TAG = '[dealSeats]: ';
    var abortReason;

    return entryRef.transaction((entry)=>{
        if (entry) {
            if ((entry.deals) && (entry.deals[data.uid])) {
                abortReason = 'You already made a reservation';
                return; // abort transaction
            } else if (entry.details.seatsCount >= data.details.seatsCount) {
                entry.details.seatsCount -= data.details.seatsCount;
                var deal = [];
                deal.status = 'asked';
                deal.details = data.details;
                if (!entry.deals) {
                    entry.deals = {};
                }
                entry.deals[data.uid] = deal;
                // Reservation is made
            } else {
                abortReason = 'Not enought seats';
                return; // abort transaction
            }
        }
        return entry;
    })
    .then((result)=>{ // resolved
        if (!result.committed) { // aborted
            return abortReason;
        } else {
            let value = result.snapshot.val();
            if (value) {
                return value;
            } else {
                return 'Offer does not exists';
            }
        }
    })
    .catch((reason)=>{ // rejected
        return Promise.reject(reason);
    });
}

The only pain is a warning during deploy in VSCode terminal about this abortions by returning no value:

warning  Arrow function expected no return value  consistent-return

currently I’m not sure if I could do anything about it.

Answer

Look at this doc in the Firebase API Reference documentation: https://firebase.google.com/docs/reference/js/firebase.database.Reference#transaction

Below is the code from this doc. Look how return; is used to abort the transaction (The doc also says: “you abort the transaction by not returning a value from your update function”). And note how this specific case is handled in the onComplete() callback function that is called when the transaction completes (within else if (!committed){} ).

// Try to create a user for ada, but only if the user id 'ada' isn't
// already taken
var adaRef = firebase.database().ref('users/ada');
adaRef.transaction(function(currentData) {
  if (currentData === null) {
    return { name: { first: 'Ada', last: 'Lovelace' } };
  } else {
    console.log('User ada already exists.');
    return; // Abort the transaction.
  }
}, function(error, committed, snapshot) {
  if (error) {
    console.log('Transaction failed abnormally!', error);
  } else if (!committed) {
    console.log('We aborted the transaction (because ada already exists).');
  } else {
    console.log('User ada added!');
  }
  console.log("Ada's data: ", snapshot.val());
});

So IMHO you should adopt the same pattern and at the places in your code where you ask “**? how to break the transaction” you do return;.

Update: You can differentiate the abortion cases by using a variable, as follows. If you add, via the Firebase console, a node age with value > 20 to users.ada.name, the first abortion cause will be “triggered”.

var adaRef = firebase.database().ref('users/ada');
var transactionAbortionCause;  //new variable
adaRef.transaction(function(currentData) {
  if (currentData === null) {
    return { name: { first: 'Ada', last: 'Lovelace' } };
  } else if (currentData.name.age > 20) {
    transactionAbortionCause = 'User ada is older than 20'; //update the variable
    console.log('User ada is older than 20');
    return; // Abort the transaction.
  } else {
    transactionAbortionCause = 'User ada already exists'; //update the variable
    console.log('User ada already exists');
    return; // Abort the transaction.
  }
}, function(error, committed, snapshot) {
  if (error) {
    console.log('Transaction failed abnormally!', error);
  } else if (!committed) {
    console.log('We aborted the transaction because ' + transactionAbortionCause);  //use the variable
  } else {
    console.log('User ada added!');
  }
  console.log("Ada's data: ", snapshot.val());
});

If I am not mistaking, you could also do that with promises, as you do in your code. The doc says that the transaction returns a non-null firebase.Promise containing {committed: boolean, snapshot: nullable firebase.database.DataSnapshot} and explains that this promise “can optionally be used instead of the onComplete callback to handle success and failure”.

So by:

  1. Doing return; for your two cases of abortion, and
  2. Reading the value of the committed boolean

you should be able to handle the abortion cases in your code by doing

.then((result)=>{
    if (result.commited) {... } else { /*abortion!*/}
}) 

I have not tested this approach however