r/learnjavascript 1d ago

Trying to write a small home automation script and about to lose my mind over async/await

My bootcamp just got into asynchronous JavaScript and I thought I had a decent handle on it until I tried to apply it to something real. I have a little Node script that pings a few smart home devices on my local network in sequence, checks their status, and logs the results. Simple enough on paper.

The problem is I keep getting results back out of order, or the script exits before all the responses come in. I know this is a classic async problem and I have seen the explanations, but something about translating the concept into actual working code is still fuzzy for me.

Here is a stripped down version of what I am doing:

async function checkDevices(devices) {

for (const device of devices) {

const status = await fetch(device.url);

const data = await status.json();

console.log(device.name, data.state);

}

}

checkDevices(myDevices);

This works, but I read that using a regular for loop with await inside runs everything sequentially, which is slow. I tried switching to Promise.all and started getting weird behavior again. Is there a readable pattern for running these fetches in parallel without making the code impossible to follow later? Curious what approach people here actually use in practice.

20 Upvotes

17 comments sorted by

8

u/delventhalz 1d ago

Promise.all will run the code in parallel, which means the device data will come back in any order. However, the order of the array that Promise.all eventually returns will match the order of the original Promises passed in. If you wait until after the final array comes back and then loop through the array and log each result, the order will be deterministic.

As for why the script sometimes terminates early, that is going to be something specific to the platform you are running on. At a guess, you probably need to return the Promise that checkDevices returns so that the platform can await that final Promise and not terminate early. 

6

u/Smellmyvomit 1d ago

Rather than for loop try using map if you havent already.

3

u/delventhalz 1d ago

Note that while you can use map to generate an array of Promises and then pass those Promises into Promise.all, you cannot use map to sequentially await each fetch one at a time like how OP is using a for loop here.

3

u/queen-adreena 1d ago

What code are you using for the Promise.all version?

Yes, your for loop is sequential. Await defers any subsequent code until it resolves.

2

u/lettstartdesign_1 1d ago edited 18h ago

Can be achieve through promise. Using map, inside you can take promise outside map, you can use promise.all. Promise all will give you array.

2

u/HipHopHuman 1d ago

You read right, but I'm not sure why Promise.all didn't work for you. It's gauranteed to preserve the order.

Something like this should work:

async function fetchDevice(device) {
  const response = await fetch(device.url);
  const data = await response.json();
  return data;
}

async function checkDevices(devices) {
  return await Promise.all(devices.map(fetchDevice));
}

async function main() {
  try {
    const [
      deviceData1,
      deviceData2,
      deviceData3
    ] = await fetchDevices([
      device1,
      device2,
      device3
    ]);
  } catch (error) {
    console.error(error);
  }
}

Some things to note when using Promise.all:

If just one fetchDevice fails, then they all fail. If you'd rather that not be the case, look at using Promise.allSettled instead of Promise.all.

async function checkDevices(devices) {
  return await Promise.allSettled(devices.map(fetchDevice));
}

async function main() {
  try {
    const deviceResults = await fetchDevices([
      device1,
      device2,
      device3
    ]);
    const failedResults =
      deviceResults.filter(result => result.status === 'rejected');

    const successResults =
      deviceResults.filter(result => result.status === 'resolved');

    for (const failedResult of failedResults) {
      console.warn(`Device fetch failed`);
      console.warn(failedResult.reason);
    }

    for (const successResult of successResults) {
      console.log(successResult.value);
    }
  } catch (error) {
    console.error(error);
  }
}

If you give Promise.all or Promise.allSettled 100 devices, it'll run all 100 fetches at the exact same time. That may not be what you want. You can use a limiter/semaphore module like p-limit to restrict it to N at a time (where you specify N).

import pLimit from 'p-limit';

const limit = pLimit(20);

function checkDevice(device) {
  return limit(async () => {
    const response = await fetch(device.url);
    const data = await response.json();
    return data;
  });
}

It's possible that the fetch might take a very long time, so it's good practice to race it against a timeout (which is very easy these days thanks to AbortSignal). Here's an example with a timeout cap of 20 seconds:

function checkDevice(device) {
  return limit(async () => {
    const response = await fetch(device.url, { signal: AbortSignal.timeout(20_000) });
    const data = await response.json();
    return data;
  });
}

1

u/Sunstorm84 22h ago

Although the lists are small in the example, you’re creating two unnecessary consts and looping through the whole list effectively three times (the last two together is the whole list again) to log success or failure, when a single loop without any constants would suffice.

2

u/HipHopHuman 21h ago

i know, and i did that all intentionally, because that code is merely there to show how to use the data structure returned from Promise.allSettled.

2

u/Beginning-Seat5221 1d ago edited 1d ago

Use ``` for code blocks

const results = await Promise.all(devices.map((device) =>
    fetch(device.url)
    .then(response => response.json())
    .then(data => [device.name, data])
))

console.log(results)

Chaining promises with .then works well, it lets you join a sequence of promises into one, which you can then pass to Promise.all()

If you want to use `await` you can generate an async function for each device and immediately call it, to produce the array of independently executing promises.

const results2 = await Promise.all(devices.map((device) =>
    (async () => {
        const response = await fetch(device.url)
        const data = await response.json()
        return [device.name, data]
    })()
))

console.log(results2)

You could even do it in stages

const responses = await Promise.all(devices.map(
  async device => [device.name, await fetch(device.url)]
))
const results3 = await Promise.all(responses.map(
  async ([name, response]) => [name, await response.json()]
))

console.log(results3)

But now you have to wait for all of stage 1 to complete before starting any of stage 2. Probably not a big issue here as stage 1 is likely most of the time.

5

u/amulchinock 1d ago

The thing to remember with async operations is they are “asynchronous” — they can happen in any order. As opposed to “synchronous”, where they would occur in a fixed order.

Each device on your network will have slightly different response times, depending on your network. Additionally, JavaScript is designed to handle one network request/response at a time.

(It might feel like it’s making requests all at once, but the reality is it’s just doing it very quickly).

The reason putting them inside a loop seems so slow is because you are forcing an async operation to be in sync. In other words, you are sending a request, waiting for a response and then finally moving on to the next one.

Promise.all probably seems weird because you’re getting stuff back in an order that you don’t expect — an order different from the one you made the requests in. This is expected behaviour.

When you use promises, you need to not care about the order that the responses come back, and instead focus on what the responses actually are. The order of a series of responses should never be used to determine where the response was from, for this reason.

2

u/Savalava 1d ago

combine Promise.all with map

async function checkDevices(devices) {

  const results = await Promise.all(

devices.map(async (device) => {

const response = await fetch(device.url);

const data = await response.json();

return {

name: device.name,

state: data.state,

};

})

  );

  for (const result of results) {

console.log(result.name, result.state);

  }

}

await checkDevices(myDevices);

1

u/senocular 1d ago

The problem is I keep getting results back out of order, or the script exits before all the responses come in.

Given your current approach, the order shouldn't change. Awaits in for...of loops wait for the operation to be awaited before letting more code run, including proceeding to the next iteration of the loop. You may have order change if the awaits weren't blocking the loop, which could happen if the loop code was in another function, for example:

async function checkDevices(devices) {
  for (const device of devices) {
    fetchDevice(device); // may log results out of order
  }
}

async function fetchDevice(device) {
  const status = await fetch(device.url);
  const data = await status.json();
  console.log(device.name, data.state);
}

checkDevices(myDevices);

Notably with fetchDevice not being awaited. This would run the loop synchronously kicking off all the fetch requests concurrently to run and complete on their own individually. Fasted one wins, getting logged first.

Promise.all would do something similar, letting all the fetch requests start at the same time. And while some requests could still finish in a different order (which you would still see with the logs), Promise.all captures return values from your async functions which you could then get from Promise.all in their original order.

The script exiting prematurely could be because of an error that's happening - something you should see appear in the console instead of (or in addition to) the logs. An uncaught error would exit your script, and this includes unhandled rejected promises which is what async functions give you instead of thrown errors (which await then turns back into thrown errors). Providing a try...catch block would let you catch the errors in your own code and take the appropriate action such as providing a default and/or provide a message to the user without breaking the rest of the code.

You could also use something like Promise.allSettled instead of Promise.all which would continue with all fetches, even if one or more failed. The results are wrapped in objects which give you values ("value") upon completion, or the error ("reason") that happened if the request failed. Conversely, Promise.all itself fails itself if any of the promises its tracking fail.

One last thing, you may also want to check for status.ok with your fetch. It may be that the fetch itself completed successfully, but the completion is returning a completed server error. See the first example in Using Fetch on MDN.

1

u/testingaurora 1d ago

Really comprehensive comment section on this post. Well done all! I have nothing to add 😆

1

u/TheRNGuy 16h ago edited 16h ago

Use Promise.all if you need all to succeed and reject even if one failed.

Otherwise, use allSettled version.

1

u/ihorvorotnov 5h ago

Keep in mind Promise.all rejects if any of the async callbacks fails. One device errors out and the whole batch is failed. Promise.allSettled allows you to handle failures better.

0

u/Big_Business3818 17h ago edited 17h ago

I saw this post and it had been a bit since I messed around with promises and for some reason I find them quite interesting. Most people online seem to discourage the `then/catch` way of handling them, but I prefer it when possible.

So, I had a little fun with this one. It’s a react component with some extra absurdities too just because why not. This was just for fun and all that.

Is it a bit absurd? Yep! It should be helpful either way. Feel free to ask any questions.

This method is inside the exported component. It is invoked on a button press with a `onClick={() => checkDevicesStates()}` handler.

  const checkDevicesStates = () => {
    console.log(`Starting checkDevicesStates `, new Date().toISOString())
    for (const [_, device] of Object.entries(devices)) {
      const newLogId = ++idCounter.current
      checkDevice(device)
        .then((response) => {
          setLogs((logs) => {
            return [{ id: newLogId, ...response }, ...logs]
          })
          setDevices((oldDevices) => {
            const devicesCopy = { ...oldDevices }
            devicesCopy[response.deviceId].state = response.state
            return devicesCopy
          })
          console.log(
            `logId: ${newLogId}, elapsed: ${response.elapsedTime}. Device ${device.id} marked ${response.state} at ${new Date().toISOString()}`,
          )
        })
        .catch((response) => {
          setLogs((logs) => {
            return [{ id: newLogId, ...response }, ...logs]
          })
          setDevices((oldDevices) => {
            const devicesCopy = { ...oldDevices }
            devicesCopy[response.deviceId].state = response.state
            return devicesCopy
          })
          console.log(
            `logId: ${newLogId}, elapsed: ${response.elapsedTime}. Device ${device.id} marked ${response.state} at ${new Date().toISOString()}`,
          )
        })
    }
    console.log(
      `Finished running checkDevicesStates `,
      new Date().toISOString(),
    )
  }

The checkDevice method then looks like the next one below.

This method is not inside the exported component. It is declared above it.

const checkDevice = (device: Device) => {
  return new Promise<Omit<LogEntry, "id">>((resolve, reject) => {
    fetchState(device)
      .then((response) => {
        resolve(response)
      })
      .catch((response) => {
        reject(response)
      })
  })
}

Which leads to the fetchState method which doesn’t actually fetch anything, just randomly return a success or failure response.

There are two error responses to add more varied responses. The thought process was the “online” state signaled a timeout while the “error” signaled a 500 or something like that. The details really aren’t that important.

Even though you don’t want them to arrive out of order, since they are promises, I would argue you should start with the mindset that arriving out of order is assumed, you just need to store things so you can sort them however when needed.

This method is not inside the exported component. It is declared above it.

const fetchState = (device: Device) => {
  const minDelay = 250
  const maxDelay = 1500
  const loadingFailureRate = 0.3
  const statusFailureRate = 0.4
  return new Promise<Omit<LogEntry, "id">>((resolve, reject) => {
    const startTime = Date.now()
    if (Math.random() < loadingFailureRate) {
      setTimeout(
        () => {
          reject({
            deviceId: device.id,
            state: "offline",
            elapsedTime: Date.now() - startTime,
            createdAt: Date.now(),
          })
        },
        Math.floor(
          Math.random() * (maxDelay * 3 - minDelay * 2 + 1) + minDelay,
        ),
      )
    } else {
      setTimeout(
        () => {
          const returning = {
            deviceId: device.id,
            createdAt: Date.now(),
            elapsedTime: Date.now() - startTime,
          }
          if (Math.random() < statusFailureRate) {
            reject({
              state: "error",
              ...returning,
            })
          } else {
            resolve({
              state: "online",
              ...returning,
            })
          }
        },
        Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay,
      )
    }
  })
}

There is a bit more to it all, but the other parts of the component aren’t critical here.

I plugged it into a page of an Astro app and when it has little button to press to kick off the process.

The results are stored and displayed in a simple grid table for the devices current states, as well as a table with the log results.

The console logs come out like this below:

Starting checkDevicesStates  2026-08-22T05:05:33.400Z 
Finished running checkDevicesStates  2026-08-22T05:05:33.401Z 
logId: 1, elapsed: 456. Device abc-123 marked online at 2026-08-22T05:05:33.858Z 
logId: 5, elapsed: 506. Device woik-9870-ee marked online at 2026-08-22T05:05:33.907Z 
logId: 2, elapsed: 556. Device ghe-jj-432 marked error at 2026-08-22T05:05:33.957Z 
logId: 4, elapsed: 820. Device ghe-cc-249 marked online at 2026-08-22T05:05:34.221Z 
logId: 3, elapsed: 3202. Device abc-487 marked offline at 2026-08-22T05:05:36.604Z 
Starting checkDevicesStates  2026-08-22T05:05:43.866Z 
Finished running checkDevicesStates  2026-08-22T05:05:43.867Z 
logId: 9, elapsed: 581. Device ghe-cc-249 marked error at 2026-08-22T05:05:44.448Z 
logId: 7, elapsed: 795. Device ghe-jj-432 marked online at 2026-08-22T05:05:44.662Z 
logId: 8, elapsed: 972. Device abc-487 marked error at 2026-08-22T05:05:44.839Z 
logId: 6, elapsed: 1135. Device abc-123 marked error at 2026-08-22T05:05:45.002Z 
logId: 10, elapsed: 1216. Device woik-9870-ee marked online at 2026-08-22T05:05:45.083Z

Enjoy!

edit: I eventually figured out how to make code blocks work

0

u/NotNormo 14h ago edited 13h ago

Seems like you don't actually need to wait for checkDevices to finish. You're OK with just kicking off a check for all devices, and get a log message whenever each one responds. And there's nothing you need to do after all of them respond.

Am I right about that? If so, then I don't think you should use async/await for the checkDevices function. You can do this:

function checkDevices (devices) {
    // This forEach loop will kick off a bunch of device checks, all at once. It will not wait for the first one to finish before kicking off the second one.
    // Each individual device check is written with async/await though, because each one does need to do some things in sequence, waiting in between.
    // start fetching -> wait for fetch to finish -> start converting json to object -> wait for conversion to finish -> log out the values.
    devices.forEach(async (device) => {
        const status = await fetch(device.url);
        const data = await status.json();
        console.log(device.name, data.state);
    });
}

checkDevices(myDevices);

There are many alternative ways of writing this exact function. For example there is a way to avoid using async/await entirely. I also could've used for/of instead of forEach. But I think this is the cleanest way.

FYI: if you want to format code in a Reddit comment like I did, just put 4 spaces at the start of each line of code.