Your approach to mocking asynchronous behavior in JavaScript with setTimeout is a relatively common practice. However, providing each function with its own invocation of setTimeout is an anti-pattern that is working against you simply due to the asynchronous nature of JavaScript itself. setTimeout may seem like it's forcing JS to behave in a synchronous way, thus producing the 4 4 4 4 then 5 5 you are seeing on alert with iteration of the for loop. In reality, JS is still behaving asynchronously, but because you've invoked multiple setTimeout instances with callbacks that are defined as anonymous functions and scoped within their own respective function as an enclosure; you are encapsulating control of JS async behavior away from yourself which is forcing the setTimeout's to run in a strictly synchronous manner.
As an alternative approach to dealing with callback's when using setTimeout, first create a method that provides the timing delay you want to occur. Example:
// timer gives us an empty named "placeholder" we can use later
// to reference the setTimeout instance. This is important because
// remember, JS is async. As such, setTimeout can still have methods
// conditionally set to work against it.
let timer
// "delayHandler", defined below, is a function expression which when
// invoked, sets the setTimeout function to the empty "timer" variable
// we defined previously. Setting the callback function as the function
// expression used to encapsulate setTimeout provides extendable control
// for us later on however we may need it. The "n" argument isn't necessary,
// but does provide a nice way in which to set the delay time programmatically
// rather than hard-coding the delay in ms directly in the setTimeout function
// itself.
const delayHandler = n => timer = setTimeout(delayHandler, n)
Then, define the methods intended as handlers for events. As a side-note, to help keep your JS code from getting messy quickly, wrap your event handler methods within one primary parent function. One (old school) way to do this would be to utilize the JS Revealing Module Pattern. Example:
const ParentFunc = step => {
// "Private" function expression for click button event handler.
// Takes only one argument, "step", a.k.a the index
// value provided later in our for loop. Since we want "clickButton"
// to act as the callback to "clickDate", we add the "delayHandler"
// method we created previously in this function expression.
// Doing so ensures that during the for loop, "clickDate" is invoked
// when after, internally, the "clickButton" method is fired as a
// callback. This process of "Bubbling" up from our callback to the parent
// function ensures the desired timing invocation of our "delayHandler"
// method. It's important to note that even though we are getting lost
// in a bit of "callback hell" here, because we globally referenced
// "delayHandler" to the empty "timer" variable we still have control
// over its conditional async behavior.
const clickButton = step => {
console.log(step)
delayHandler(8000)
}
// "Private" function expression for click date event handler
// that takes two arguments. The first is "step", a.k.a the index
// value provided later in our for loop. The second is "cb", a.k.a
// a reference to the function expression we defined above as the
// button click event handler method.
const clickDate = (step, cb) => {
console.log(step)
cb(delayHandler(8000))
}
// Return our "Private" methods as the default public return value
// of "ParentFunc"
return clickDate(step, clickButton(step))
}
Finally, create the for loop. Within the loop, invoke "ParentFunc". This starts the setTimeout instance and will run each time the loop is run. Example:
for(let i = 0; i < 4; i++) {
// Within the for loop, wrap "ParentFunc" in the conditional logic desired
// to stop the setTimeOut function from running further. i.e. if "i" is
// greater than or equal to 2. The time in ms the setTimeOut was set to run
// for will no longer hold true so long as the conditional we want defined
// ever returns true. To stop the setTimeOut method correctly, use the
// "clearTimeout" method; passing in "timer", a.k.a our variable reference
// to the setTimeOut instance, as the single argument needed to do so.
// Thus utilizing JavaScript's inherit async behavior in a "pseudo"
// synchronous way.
if(i >= 2) clearTimeout(timer)
ParentFunc(i)
}
As a final note, though instantiating setTimeOut is common practice in mocking asynchronous behavior, when dealing with initial invocation/execution and all subsequent lifecycle timing of the methods intended to act as the event handlers, defer to utilizing Promises. Using Promises to resolve your event handlers ensures the timing of method(s) execution is relative to the scenario in which they are invoked and is not restricted to the rigid timing behavior defined in something like the "setTimeOut" approach.