Retrieving my public IP address with the javascript Fetch API (no jquery, no json) Note for future reference and google searches
The list of urls for IP retrieving will very likely need continuous adjustments in the future.
// Few services that should return the IP as plain text,
// properly configured to accept CORS
const IP_URLs = [
  'https://api.ipify.org/',
  'https://ipecho.net/plain',
  'https://api.kwelo.com/v1/network/ip-address/my',
  'https://myexternalip.com/raw',
];


Original question
This code is for retrieving the public IP address. I wanted something that can automatically switch between a few services in case the some of them are down. Since I do not use jquery in my project, I preferred the Fetch API aiming at keeping the code as light as possible. I was not able to find anything similar in the web, so here we go!
I am mostly concerned about correctness, but since I consider myself a beginner in JS, also readability, style, indentation are all very relevant. Should I have used async/await?
// Few services that should return the IP as plain text, properly configured to accept CORS
const IP_URLs = [
  'https://api.ipify.org/',
  'https://ipecho.net/plain',
  'https://api.kwelo.com/v1/network/ip-address/my',
  'https://myexternalip.com/raw',
];

// Log my public IP in the console
get_ip(IP_URLs, console.log);

// Trampoline that deep copies the urls array to later allow for pop() and recursion
function get_ip(urls, callback) {
  get_ip_impl(urls.slice(), callback);
}

function get_ip_impl(urls, callback) {
  if (urls.length &gt; 0) {
    let url = urls.pop();
    let init = {
      method: 'GET', // *GET, POST, PUT, DELETE, etc.
      mode: 'cors', // no-cors, *cors, same-origin
      cache: 'no-store', // *default, no-cache, reload, force-cache, only-if-cached
      credentials: 'same-origin', // include, *same-origin, omit
      headers: {
        'Content-Type': 'application/text'
      },
      redirect: 'follow', // manual, *follow, error
      referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
    };    
    fetch( url, init).then(
      response =&gt; {
        response.text().then(
          txt   =&gt; { if (isValidIP(txt)) {
                       callback(txt);
                     } else {
                       console.log(&quot;Invalid IP: &quot;+url+&quot; --&gt; &quot;+txt);
                       get_ip_impl(urls, callback);
                     }
                   },
          notxt =&gt; { console.log(&quot;text(): &quot;+url+&quot; --&gt; &quot;+notxt);
                     get_ip_impl(urls, callback);
                   }
        )
      },
      reason =&gt; { console.log(&quot;Fetch: &quot;+url+&quot; --&gt; &quot;+reason);
                  get_ip_impl(urls, callback); }
    );
  } else {
    callback(&quot;No more URLs&quot;);
  }
}

function isValidIP(txt) {
  return /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/.test(txt);
}
One of the main reasons Promises were created was to avoid the ugly nesting of callbacks when multiple asynchronous actions need to occur. For example, rather than

fn1(dataToSend, dataForFn2 =&gt; {
  fn2(dataForFn2, dataForFn3 =&gt; {
    fn3(dataForFn3, processedData =&gt; {
      console.log(processedData);
    });
  });
});


If the functions returned Promises instead of accepting callbacks, the above could be done with

fn1(dataToSend)
  .then(fn2)
  .then(fn3)
  .then(processedData =&gt; {
    console.log(processedData);
  });


The Promise-as-callback antipattern is when you nest .thens in other .thens or pass around callbacks rather than chaining another .then onto the previous Promise. This is the trap you're falling into here. Since fetch returns a Promise itself, it would be best to return that Promise and allow consumers of this script to use it. That is, it would be really convenient (and appropriate) if consumers to be able to do something like

get_ip(IP_URLs)
  .then(console.log)
  .catch(handleErrors);


Another related nesting issue you might consider is, instead of

if (goodCondition) {
  // lots of code
} else {
  callback("No more URLs");
}


Rather than having to keep track of which brackets correspond to what upper condition, it might be preferable to return early instead:

if (!goodCondition) {
  callback("No more URLs");
  return;
}
// lots of code


But if you return the Promises instead of using callbacks, this should cease to be an issue.

The init object you're constructing seems strangely complicated. Many of the properties you're passing are the default ones already (like method: 'get', redirect: 'follow') or useless (credentials: 'same-origin'). Unless you're deliberately trying to affect some things with the object, it would be simpler and less confusing to omit it entirely (the script looks to work just fine without it, none of the properties look useful).

You have a lot of fail-handlers. If you really want to exhaustively identify what's OK and what isn't, it would be good to also check if the response is OK too:

return fetch(url).then(
  response =&gt; {
    if (!response.ok) {
      console.log('Response was not OK:', response.status);
      return getIPFromNextSite(urls);
    }


But unless you're actively analyzing exactly which stage is working and what isn't, and really need all those .fail handlers, you might consider avoiding them and .catching errors at the end instead:

function getIPFromNextSite(urls) {
  if (!urls.length) {
    // Send control flow to the consuming .catch block:
    throw new Error('No more URLs');
  }
  const url = urls.pop();
  return fetch(url)
    .then(response =&gt; response.text())
    .then((txt) =&gt; {
      if (isValidIP(txt)) {
        // Send control flow to the consuming .then block:
        return txt;
      } else {
        return getIPFromNextSite(urls);
      }
    })
    .catch((err) =&gt; {
      return getIPFromNextSite(urls);
    });
}


It's only a comment in your code, but .slice does not deep copy arrays - it only shallow copies them. (Since the array here contains immutable strings, a shallow copy is fine anyway.)

If you want to follow the standard Javascript naming conventions, you could consider using camelCase for functions and most variables by default.

When declaring variables, best to always use const. Using let warns readers of the code that you may reassign the variable name, which results in more cognitive overhead (and is confusing if there isn't actually any chance of the name being reassigned).

In full, this is how I would do it:



const IP_URLs = [
  'https://api.ipify.org/',
  'https://ipecho.net/plain',
  'https://api.kwelo.com/v1/network/ip-address/my',
  'https://myexternalip.com/raw',
];
function getIPFromNextSite(urls) {
  if (!urls.length) {
    // Send control flow to the consuming .catch block:
    throw new Error('No more URLs');
  }
  const url = urls.pop();
  return fetch(url)
    .then(response =&gt; response.text())
    .then((txt) =&gt; {
      if (isValidIP(txt)) {
        // Send control flow to the consuming .then block:
        return txt;
      } else {
        return getIPFromNextSite(urls);
      }
    })
    .catch((err) =&gt; {
      return getIPFromNextSite(urls);
    });
}
function isValidIP(txt) {
  return /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/.test(txt);
}
const getIP = urls =&gt; getIPFromNextSite(urls.slice());

getIP(IP_URLs).then(console.log);





  Should I have used async/await?


You could. If you need to exhaustively check every error path, it would require a lot of ugly try/catch boilerplate, but if you just want a result, it looks moderately cleaner than the version above:



const IP_URLs = [
  'https://api.ipify.org/',
  'https://ipecho.net/plain',
  'https://api.kwelo.com/v1/network/ip-address/my',
  'https://myexternalip.com/raw',
];
async function getIPFromNextSite(urls) {
  for (const url of urls) {
    try {
      const response = await fetch(url);
      const text = await response.text();
      if (isValidIP(text)) {
        // Send control flow to the consuming .then block:
        return text;
      }
    } catch(e) {
      // Don't do anything, just continue on to next iteration
    }
  }
  // Send control flow to the consuming .catch block:
  throw new Error('No more URLs');
}
function isValidIP(txt) {
  return /^(?!0)(?!.*\.$)((1?\d?\d|25[0-5]|2[0-4]\d)(\.|$)){4}$/.test(txt);
}
const getIP = urls =&gt; getIPFromNextSite(urls);

getIP(IP_URLs).then(console.log);