getAttrs() is an asynchronous function - the callback won't run until it's fetched the requested Attributes via a Promise. JS will continue to execute code until the Promise is resolved and reaches the front of the queue. Then the callback starts. You have very little control over this in synchronous JS - the only thing you can be sure of is that your callback function won't run until the parent function has resolved. The simplest way to make sure the last bit of code runs after the rest is to put it inside the callback. But you might have a reason for not wanting to do that, not sure. In that case, you could also rewrite it as an async function, giving you more control over sequencing. You can await each time you're calling an asynchronous function (you can await any line you want, it just won't do anything to synchronous code): on("change:stamina change:shielding", async function(eventInfo) { var statMax = 0; if (eventInfo.info === 'sheetworker') return; var stat = eventInfo.sourceAttribute; await Promise.all(modifiers.map(function (stat) { getAttrs([stat], function(values) { var multiplier = stat === 'legsLevel' ? 3 : stat === 'armsLevel' ? 3 : stat === 'bonesLevel' ? 5 : stat === 'nervesLevel' ? 5 : stat === 'brainLevel' ? 5 : stat === 'sensesLevel' ? 5 : 1; var change = values[stat] * multiplier; console.log(stat + ' ' + multiplier); statMax = statMax + change; console.log(statMax); }); })); var v = eventInfo.newValue || 0; var value = v < 0 ? 0 : v > statMax ? statMax : v; console.log(value); await setAttrs({ [stat]: value }); console.log(`Finished`); }); I can't test this properly as I don't have Pro, but I think that should now run in the order you want it to. One issue though - you have your getAttrs() inside the loop. getAttrs is one of the slower functions on a sheet, you generally want to grab all the Attributes you need with a single execution before you start iterating over them. You're also using stat twice - you've got it declared as a hoisted variable, then used it as the iterable name in your forEach loop. That's left me a bit confused about what exactly is supposed to be going on in the function - where is the var stat = eventInfo version supposed to be being used? I'm guessing that version of stat is only supposed to go at the end for the setAttrs. Oh, and the function will probably run twice if both the events you've specified were triggered.... I think! Anyway, my best guess as to what you're trying to do (with the stat confusion), I think this might work better for you: const modifiers = ['staminaMax', 'passiveStamina', 'aggravatedValue', 'legsLevel', 'armsLevel', 'bonesLevel', 'nervesLevel', 'brainLevel', 'sensesLevel']; on("change:stamina change:shielding", async function(eventInfo) { let statMax = 0; if (eventInfo.info === 'sheetworker') return; let triggerStat = eventInfo.sourceAttribute; await getAttrs(modifiers, async function(values) { modifiers.forEach((mod) => { let multiplier = mod === 'legsLevel' ? 3 : mod === 'armsLevel' ? 3 : mod === 'bonesLevel' ? 5 : mod === 'nervesLevel' ? 5 : mod === 'brainLevel' ? 5 : mod === 'sensesLevel' ? 5 : 1; let change = parseInt(values[mod],10) * multiplier; console.log(mod + ' ' + multiplier); statMax = statMax + change; console.log(statMax); }); }); let v = eventInfo.newValue || 0; let value = v < 0 ? 0 : v > statMax ? statMax : v; console.log(value); await setAttrs({ [triggerStat]: value }); console.log(`Finished`); }); I've changed the var s to let s, as it's generally better to avoid var unless you need your variable to be global. Change them back if you do need those variables in other parts of the code. getAttrs now just grabs all the stats once, then does the iteration, and finally triggerStat (the stat that triggered the event listener) is saved with the calculated value. I might have messed up your calculations though! The async code is probably superfluous at this point, as you can probably just put the setAttrs and accompanying calculations inside the getAttrs callback (but after the forEach loop!) and run it all synchronously.