Search code examples
javascripthtmlfetch-apies6-promisetail-recursion

How can I avoid recursion in this JavaScript function that uses Promises?


I have a JavaScript function that generates an endless slideshow. The code works, but I'm concerned about each iteration ending in a recursive call to itself. Using developer tools, it appears that the call stack is growing.

I tried removing the recursive call to show_slides and wrapping the body of the function in a while (true) loop, but this caused the browser to lock up, burning lots of CPU and displaying nothing.

I assume I've missed waiting for the resolution of a Promise somewhere, and have tried appending .then(() => {}) to both the inner and the outer chains, without success.

Assuming that this is growing the stack (infinitely), rather than being treated as tail recursion and actually executing as a loop, what do I need to turn the body of the function into a loop?

Update: screenshot of Chrome dev tools call stack after a few iterations:

Call stack

const duration = 2000; // time (msec) to display each slide
const sizes = [
  [4000, 500],
  [1000, 4000],
  [600, 400],
  [100, 200],
  [4000, 4000]
];
const sleep = ms => new Promise(r => setTimeout(r, ms));
let n = 0;

function show_slides(duration) {
  const my_parent = document.querySelector('#slide-div');
  let size_index = n++ % sizes.length;
  let w = sizes[size_index][0];
  let h = sizes[size_index][1];

  let my_randomizer = `https://placehold.co/${w}x${h}?text=${w}+x+${h}\npx`;
  fetch(my_randomizer)
    .then(my_response => my_response.blob())
    .then(my_blob => {
      let my_url = URL.createObjectURL(my_blob);
      sleep(duration)
        .then(() => {
          URL.revokeObjectURL(my_parent.querySelector('img').src);
          my_parent.querySelector('img').src = my_url;
          // A recursive call to iterate??
          show_slides(duration);
        });
    })
    .catch(my_error => console.error('Error: ', my_error));
}
* {
  box-sizing: border-box;
}

html {
  height: 100%;
  width: 100%;
}

body {
  background-color: #dbd2c3;
  font-family: Arial, Helvetica, sans-serif;
  /* prevent body from displacing */
  margin: 0;
  /* body should perfectly superimpose the html */
  height: 100%;
  width: 100%;
}

.outer-div {
  display: flex;
  flex-flow: column;
  height: 100%;
  /* Now create left/right margins */
  margin: 0 0;
}

.inner-fixed-div {
  margin-top: 0.5em;
}

.inner-remaining-div {
  margin-bottom: 0;
  flex-grow: 1;
  /* hints the contents to not overflow */
  overflow: hidden;
}

.picture-div {
  /* force the div to fill the available space */
  width: 100%;
  height: 100%;
  /* center child elements */
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

.picture-div-img {
  max-width: calc(100% - 0.5em);
  /* included 0.5em * 2 margin on parent */
  max-height: calc(100% - 0.5em);
  /* included 2em margin on parent, may need adjust this further */
  border: 2px solid black;
}
<!DOCTYPE html>
<html lang="en">
<!-- Self-contained slideshow demo -->

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">

</head>

<body onload="show_slides(duration);">
  <div class="outer-div">
    <div class="inner-fixed-div">
      <h1>Lorem Ipsum</h1>
    </div>
    <div class="inner-remaining-div">
      <div id="slide-div" class="picture-div">
        <!-- Placeholder <img> element for slides -->
        <img class="picture-div-img">
      </div>
    </div>
  </div>
</body>

</html>


Solution

  • See Why does setTimeout() clutter my call stack under Chrome DevTools? – the Chrome developer tools automatically capture an asynchronous “stack trace” for debugging. Your code is correct, and the call stack isn’t actually growing. (I’m not sure exactly how the developer tools deal with infinite growth, but they probably truncate somewhere; a stack overflow would be a bug in the developer tools.)

    Still, as long as you can rely on support for async/await, you can get both a cleaner debugging experience and cleaner code:

    const duration = 2000; // time (msec) to display each slide
    const sizes = [
      [4000, 500],
      [1000, 4000],
      [600, 400],
      [100, 200],
      [4000, 4000],
    ];
    const sleep = ms => new Promise(r => setTimeout(r, ms));
    
    async function show_slides(duration) {
      const my_img = document.querySelector('#slide-div img');
      let n = 0;
    
      while (true) {
        const size_index = n++ % sizes.length;
        const [w, h] = sizes[size_index];
      
        let my_randomizer = `https://placehold.co/${w}x${h}?text=${w}+x+${h}\npx`;
        try {
          const my_response = await fetch(my_randomizer);
          const my_blob = await my_response.blob();
          const my_url = URL.createObjectURL(my_blob);
          URL.revokeObjectURL(my_img.src);
          my_img.src = my_url;
        } catch (my_error) {
          console.error('Error: ', my_error);
          break;  // or continue after a delay
        }
        
        await sleep(duration);
      }
    }
    * {
      box-sizing: border-box;
    }
    
    html {
      height: 100%;
      width: 100%;
    }
    
    body {
      background-color: #dbd2c3;
      font-family: Arial, Helvetica, sans-serif;
      /* prevent body from displacing */
      margin: 0;
      /* body should perfectly superimpose the html */
      height: 100%;
      width: 100%;
    }
    
    .outer-div {
      display: flex;
      flex-flow: column;
      height: 100%;
      /* Now create left/right margins */
      margin: 0 0;
    }
    
    .inner-fixed-div {
      margin-top: 0.5em;
    }
    
    .inner-remaining-div {
      margin-bottom: 0;
      flex-grow: 1;
      /* hints the contents to not overflow */
      overflow: hidden;
    }
    
    .picture-div {
      /* force the div to fill the available space */
      width: 100%;
      height: 100%;
      /* center child elements */
      display: flex;
      flex-direction: column;
      justify-content: center;
      align-items: center;
    }
    
    .picture-div-img {
      max-width: calc(100% - 0.5em);
      /* included 0.5em * 2 margin on parent */
      max-height: calc(100% - 0.5em);
      /* included 2em margin on parent, may need adjust this further */
      border: 2px solid black;
    }
    <!DOCTYPE html>
    <html lang="en">
    <!-- Self-contained slideshow demo -->
    
    <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width, initial-scale=1">
    
    </head>
    
    <body onload="show_slides(duration);">
      <div class="outer-div">
        <div class="inner-fixed-div">
          <h1>Lorem Ipsum</h1>
        </div>
        <div class="inner-remaining-div">
          <div id="slide-div" class="picture-div">
            <!-- Placeholder <img> element for slides -->
            <img class="picture-div-img">
          </div>
        </div>
      </div>
    </body>
    
    </html>