var has function scope.
let has block scope.
So, in a for loop like this:
for (let i = 0; i < row.length; i++) 
Each iteration of the loop has it's own separate i variable.  When using asynchronous operations inside the loop, that keeps each separate i variable with it's own asynchronous callback and thus one doesn't overwrite the other.
Since var is function scoped, when you do this:
for (var i = 0; i < row.length; i++) 
There's only one i variable in the whole function and each iteration of the for loop uses the same one.  The second iteration of the for loop has changed the value of i that any asynchronous operations in the first iteration may be trying to use.  This can cause problems for those asynchronous operations if they are trying to use their value of i inside an asynchronous callback because meanwhile the for loop will have already changed it.
Here's a classic example:
for (var i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 10);
}
 
 
And, now with let
for (let i = 0; i < 5; i++) {
    setTimeout(function() {
        console.log(i);
    }, 10);
}
 
 
Run each one and see the difference in results.  In both examples, the for loop runs to completion and schedules 5 timers.  In the var example, all the timer callbacks see the one i variable from the now finished for loop and thus they all output 5 as the value of i.
In the let example, each iteration of the loop has its own i variable that is unaffected by the march of the for loop so when their timer callback is called (after the for loop has finished), they still have the same value of i that they had when their iteration of the for loop was originally executed and thus they output the expected sequence of increasing numbers.