Custom setInterval to run function synchronously This customSetInterval function makes sure that the function inside it is executed synchronously. It also gives that function the ability to determine run or stop the timer. The object returned by customSetInterval has stop function to stop the timer explicitly, later it could have more properties related to the timer status if needed.

What do you think about this function? Are there any possible problems? I am concerned whether it could cause any memory leak issue.

This is my customSetInterval function:

function customSetInterval(func, interval, data, handler = {
  timerId: null,
  stop() {
    return clearTimeout(this.timerId);
  }
}) {
  const isContinue = func(data);

  if (isContinue) {
    handler.timerId = setTimeout(
        customSetInterval,
        interval,
        func,
        interval,
        data,
        handler
    );
  }

  return handler;
}
Not much to review but
A few points
Too many arguments. You have a lot of arguments that could actually be passed as  just the handler, and if you make the timeout callback one declared in the customSetInterval (rather than call customSetInterval) you can close over some of the variables and thus would not have to pass them (and keep them protected).
The variable isContinue could be removed as it is only used once and may be better as
if (func(data)) {
    handler.timerId = setTimeout(...

The first call to customSetInterval executes the function immediately, this is not what setTimeout does. On the first pass should it not set a timeout rather than execute?
Memory leaks?
That would be up to the functions being called. Your timeout will eventually release any references that are held pending the timeout so you are not causing a leak. There could be memory problems (not a leak just running low) if there are a lot of timeouts and the delay between them is long, but not a real concern.
Timing issues.
You set the timeout after you call the function and do not account for the time spent running the function. The timeouts will always be late and any error in time will accumulate with each call by the amount of time the func takes to execute (and any other page blocking events).
The way to deal with this is to record the start time and adjust the timeout to be at the correct interval.
That then introduces the issue of catching up to late calls. There are any number of ways the page can blocked making your calls is late. If late do you still make the call or skip it. If you make the call and the function you call is taking longer than the interval do you create a stack of pending calls?
A quick rewrite
As a suggestion only to demonstrate the use of closure to keep stats on the timeout and keeping track of the next scheduled time out, adjusting the time to keep things on time if possible (if more than half a interval behind the timeout is skipped) (though I did not test that)


function customSetInterval(handler) {
  var id, interval, count;
  const startTime = performance.now();

  // this returns the time till next interval. If late will drop calls.
  function getNextCallTime() {
    var nextCallIn = (startTime + interval * (count + 1)) - performance.now();
    if (nextCallIn &lt; -interval / 2) { // to late drop the call
      count = Math.floor((performance.now() - startTime) / interval) + 1;
      nextCallIn = (startTime + interval * count) - performance.now();
    }
    return nextCallIn;
  }
  
  // This handles the callback
  function execute(handler) {
    if (handler.func(handler)) {
      count += 1;
      id = setTimeout(execute, getNextCallTime(), handler);
    }
  }
  handler = Object.assign({
    interval: 1000,
    stop() {
      return clearTimeout(id)
    },
  }, handler);
  count = 0;
  interval = handler.interval;
  id = setTimeout(execute, getNextCallTime(), handler);
  return handler;
}



// Tests as examples how to.


function run(handler) {
  log("Tick for " + handler.data.name);
  handler.data.counter -= 1;
  if (handler.data.counter === 0) {
    log("Run complete for " + handler.data.name, "red");
    return false;
  }
  return true;
}

function killNumThree(handler) {
  log("Assasin Killing number 3");
  handler.data.handleFor3.stop();
  log("Run complete for " + handler.data.name, "red");
  log("No more intervals expected!", "red");
}

// every second ten times
customSetInterval({
  func: run,
  interval: 1000,
  data: {
    counter: 10,
    name: "Number ONE"
  }
});

// every half second 10 times
customSetInterval({
  func: run,
  interval: 500,
  data: {
    counter: 10,
    name: "Number Two"
  }
});

// every 10th second 20 times
customSetInterval({
  func: run,
  interval: 100,
  data: {
    counter: 20,
    name: "Number Two and a half"
  }
});


// every 2.5 seconds 1000 times
const num3Handle = customSetInterval({
  func: run,
  interval: 2500,
  data: {
    counter: 1000,
    name: "Number Three"
  }
});


// once at 11 seconds and stops number 3
customSetInterval({
  func: killNumThree,
  interval: 11000,
  data: {
    name: "Assasin",
    handleFor3: num3Handle
  }
});



var last;

function log(data, col = "black") {
  var div = document.createElement("div");
  div.textContent = data;
  div.style.color = col;
  if (!last) {
    document.body.appendChild(div);
  } else {
    document.body.insertBefore(div, last);
  }
  last = div;
}