First, let me lay out what I want to do, in the simplest terms:
I have a series of functions that need to be called in a particular order; each function in that order has its own order it needs to follow.
My current code, simplified:
async function LastModuleFunction() {
await FirstModuleFunction(); // Order:1
await SecondModuleFunction(); // Order: 2
// ...etc...
await EighthModuleFunction(); // Order: 8
setTimeout(GoToNextPage(), 2000); // Order 9
}
function GoToNextPage() {
// some script irrelevant to this question...
window.location.href = 'OtherPage.aspx'; // Order: LAST
}
lastModule.js
async function FirstModuleFunction() {
var obj = { item1: 0, item2: 0, item3: '' }; // you get the idea
FirstModuleDataSaveProcess();
async function FirstModuleDataSaveProcess() {
await FirstModuleDataFunction1(); // Order: 1A
await FirstModuleDataFunction2(); // Order: 1B
// I created a shared function for AJAX
PostDataAjax('AutoSave_ModuleData.asmx/AutoSave_FirstModule', obj, true); // Order: 1C
// The last parameter is just a boolean to tell the function if it should fire a "Saved!"
// message (via Toast) upon success.
}
}
firstModule.js
$('#btnSaveAndFinish').on('click', async function() {
$(this).prop('disabled', true);
$(this).html(waitSpinner); // just a bootstrap "please wait" animation here
LastModuleFunction();
});
moduleNavigation.js
The order MUST go in this pattern: 1A -> 1B -> 1C -> 2A -> 2B -> ... -> LAST
Each function in the sequence MUST wait for the previous to complete.
Boring backstory & context:
I have an aspx page with multiple UserControl modules (ascx). Which UC's, and how many, depend on the user and the time of day (it's a meeting recorder app that handles 4 different types of meetings each day).
Each time the user navigates to the next module (they come in on a bootstrap carousel), a series of functions prepares the data and sends it to a WebService class (asmx) via AJAX (see firstModule.js above). We tell the user this "auto-saves" the data as they go (which it does).
On the last module, the "next" button is replaced by a "save and finish" button. When clicked, this button goes through EACH of the previous modules again*, saves ALL form data to the database through each AJAX function, and THEN location.href
's to a different aspx page.
*The user is also able to skip ahead or back to a module, which doesn't trigger an "auto-save," so the app makes extra-sure all data is accounted for.
What's actually happening:
Most of the time, this works as intended. The functions fire in order, and the 2-second buffer I give LastModuleFunction()
is enough time to have the data saved and ready for the next page before it redirects.
...But sometimes, it doesn't/isn't.
There are two significant tells that indicate something's not right:
In
FirstModuleFunction()
, the WebService called in the AJAX updates a data row that holds the core information for that meeting; the "MeetingCompleted" column in that data row is changed from 'N' to 'Y'. The next page's code, atPage_Load
, checks if this value is indeed a 'Y'; if not, it redirects back to the original page with a specific query string that tells me that the check failed. So it looks like, in some cases,FirstModuleFunction()
doesn't complete before the redirect fires.When placing
console.log()
calls before each function inLastModuleFunction()
, they fire in the correct sequence on the console, each time I tested. However, when I place anotherconsole.log
insideFirstModuleFunction()
, for example, the console shows a different story:
1st module hit
2nd module hit
3rd module hit
function inside 1st module hit <-- what the...?
4th module hit
[etc.]
console
I'm now starting to think that simply using await
when calling async
functions isn't getting the job done. I can't remember where I read it would be this simple, but it appears I either misunderstood or was misinformed, hm? Apparently I need to use Promises...is that correct? Problem is, I could never wrap my head around how to make promises work with this exact sequence structure I need. Can someone help me understand what kind of structure I should be using?
Note: most of these functions do not return any value; they simply act as routines.
CodePudding user response:
Fix #1: Maintaining Sequence (easier than I thought)
Thanks to Dave's help, I was able to solve part of the problem. If a function is part of a sequence of await
functions, and it has its own sequence of child functions that also use await
, add the await
to ALL of the child functions, else the next "parent" function won't wait for the last child function to finish. It doesn't matter if the function is called last; if SecondModuleFunction()
is an await
function, then PostDataAjax
might end up waiting for it, unless it is given an await
, and your order might be thrown off. When I applied this fix, the console gave me a perfect sequence, every time (1, 1a, 1b, 1c, 2, 2a, 2b, 3, 3a, ...etc.).
async function FirstModuleFunction() {
var obj = { item1: 0, item2: 0, item3: '' };
await FirstModuleDataSaveProcess(); // <--await added here
async function FirstModuleDataSaveProcess() {
await FirstModuleDataFunction1();
await FirstModuleDataFunction2();
await PostDataAjax('AutoSave_ModuleData.asmx/AutoSave_FirstModule', obj, true); // <--await added here
}
}
In other words: when in doubt, await it.
Fix #2: Waiting for AJAX (more tricky)
The bigger problem, I discovered, had to do with that bug I mentioned--where the AJAX wasn't updating the data row server-side, in the background (AJAX -> asmx subroutines) before the page redirected. All of my functions were firing in perfect order, but without the redirect properly waiting for the AJAX response to the client, none of that mattered.
Sometimes the setTimeout
(in my case, 2 seconds) gives it enough time; sometimes it doesn't. I hypothesized that if I added await
to the $.ajax
call itself, I was only making the script wait for the post, and not the response from the server. It turned out I was correct.
In this case, the solution ended up being a combination of two popular "make the script wait" solutions. One was, of course, await
. The other was to add a callback:
async function PostDataAjax(subdir, dataObj, showSuccess) {
// Toast customization script went here
// ...
// For the callback, we just need SOMETHING. ANYTHING. Don't complicate it.
await PostData(Toast, simpleCallback); // Of course, I added an await. Just in case. ;)
async function PostData(Toast, callback) {
await $.ajax({ // Again, awaiting just in case.
type: 'POST',
url: 'Ajax/' subdir,
contentType: 'application/json; charset=utf-8',
dataType: 'json',
data: JSON.stringify(dataObj),
success: function (result) {
if (showSuccess === true) {
Toast.fire({
title: 'Saved!',
icon: 'success',
timer: 2000
});
}
callback(); // I'm calling callback both here and in error handling
},
error: function (xhr, status, error) {
Toast.fire({
title: 'Error:',
text: 'Trouble saving data. Please contact the Web Team. (' error ')',
icon: 'error'
});
callback();
}
});
}
function simpleCallback() { // As I said: don't complicate it. This is as simple as it gets.
return false;
}
}
Not only did this work, but it allowed me to go ahead and remove the 2-second setTimeout
from the GoToNextPage()
call. Using console.log()
markers, I was able to watch the console take its time going through each AJAX-related function when I clicked the "finish and save" button. Everything ran in sequence, and did not let the page redirect until it was done. Exactly what my goal was.
Additional note (2/28/2022):
To be thorough, I should also point out: obviously, you can (and should) remove await
s where not necessary, after testing what works best in your script. Be especially careful about adding awaits inside of for
or each
loops; sure, it might add only a tenth or two of a second to your load time, but if you don't need it there, why keep it?