# JavaScript Async Lab

These exercises run locally; network requests are fixtures.

## 1. Return from an async function

Calling an async function always returns a promise. A returned ordinary value becomes its fulfillment value. The caller can await that promise; async does not make CPU-heavy code run on another thread.

Return data.value doubled.

Hint: Return the calculation; async wraps its result.

```javascript
async function transform(data) {
  return data.value * 2;
}
```

## 2. Await a fulfilled value

await obtains a promise’s fulfillment value. Execution after await resumes asynchronously, even when the promise is already fulfilled. This pauses this function, not the entire browser.

Await Promise.resolve(data), then return its name.

Hint: Assign the awaited result to a variable.

```javascript
async function transform(data) {
  const user = await Promise.resolve(data);
  return user.name;
}
```

## 3. Chain a transformation

then returns a new promise. Its callback return value becomes the next fulfillment value. Forgetting return in a block-bodied callback produces undefined.

Use a promise chain to trim and uppercase a string.

Hint: Each then callback must return the next value.

```javascript
async function transform(data) {
  return Promise.resolve(data).then(s => s.trim()).then(s => s.toUpperCase());
}
```

## 4. Catch a rejection

A rejected promise becomes a thrown value at await. Put the await inside try/catch to handle it. Rejections can contain arbitrary values, but Error objects provide useful messages and stacks.

If data.ok is false, reject with Error("Unavailable"); return "fallback" on failure, otherwise data.value.

Hint: A catch block can return a replacement value.

```javascript
async function transform(data) {
  try {
    if (!data.ok) await Promise.reject(new Error("Unavailable"));
    return data.value;
  } catch { return "fallback"; }
}
```

## 5. Preserve an error message

A catch handler can translate a failure into a structured result. Returning an error record fulfills the outer promise; it is a deliberate API contract rather than a rejected promise.

Reject with Error(data), catch it, and return {ok:false, message:error.message}.

Hint: Read message from the caught Error.

```javascript
async function transform(data) {
  try { await Promise.reject(new Error(data)); }
  catch (error) { return {ok:false, message:error.message}; }
}
```

## 6. Always clean up with finally

finally runs after try or catch, including before a pending return finishes. Avoid returning from finally: it can override the earlier return or error. Here the log array makes cleanup observable.

Return a log: "start", then "success" or "failure" from data.ok, then "cleanup".

Hint: Push cleanup inside finally, then return after the block.

```javascript
async function transform(data) {
  const log = ["start"];
  try { if (!data.ok) throw new Error("failed"); await Promise.resolve(); log.push("success"); }
  catch { log.push("failure"); }
  finally { log.push("cleanup"); }
  return log;
}
```

## 7. Collect with Promise.all

Promise.all produces values in input order, even when operations finish in a different order. Creating promises starts the operations; all coordinates their results. One rejection rejects the aggregate and does not cancel other work.

Resolve each number doubled and return all results.

Hint: An async map callback returns promises; collect them with all.

```javascript
async function transform(data) {
  return Promise.all(data.map(async n => n * 2));
}
```

## 8. Handle an aggregate failure

Promise.all is useful when every result is required. Catching its rejection lets a caller choose a fallback for the whole group. Work already started may still finish after the rejection.

Resolve nonnegative numbers; reject negative numbers. Return the array on success or "batch failed".

Hint: Use return await inside try so catch receives the rejection.

```javascript
async function transform(data) {
  try { return await Promise.all(data.map(n => n < 0 ? Promise.reject(new Error("negative")) : Promise.resolve(n))); }
  catch { return "batch failed"; }
}
```

## 9. Inspect all settled statuses

allSettled waits until every input settles. Each result has status plus value for fulfillment or reason for rejection. It is useful for independent tasks where partial success still matters.

For each boolean, fulfill true and reject false; return the settled status strings.

Hint: Map settled records to status.

```javascript
async function transform(data) {
  const results = await Promise.allSettled(data.map(ok => ok ? Promise.resolve(true) : Promise.reject(new Error("failed"))));
  return results.map(r => r.status);
}
```

## 10. Keep partial successes

After allSettled, filter by status before reading value. A fulfilled value can be zero, false or null; filtering by truthiness would incorrectly discard legitimate data.

Resolve nonnegative numbers and reject negatives; return only fulfilled values in input order.

Hint: Filter by status, not by value.

```javascript
async function transform(data) {
  const results = await Promise.allSettled(data.map(n => n >= 0 ? Promise.resolve(n) : Promise.reject(new Error("negative"))));
  return results.filter(r => r.status === "fulfilled").map(r => r.value);
}
```

## 11. First fulfillment with any

Promise.any fulfills on the first fulfillment, ignoring rejections unless all inputs reject. In that case it rejects with AggregateError. An empty input also rejects. This fixture uses already-settled promises, not network timing.

Return the first nonnegative number using Promise.any, or null if none exists.

Hint: Catch the all-rejected case.

```javascript
async function transform(data) {
  try { return await Promise.any(data.map(n => n >= 0 ? Promise.resolve(n) : Promise.reject(new Error("negative")))); }
  catch { return null; }
}
```

## 12. Race the first settlement

race adopts the first settlement, whether fulfillment or rejection. For already-settled inputs, ordering of scheduled reactions matters. Promise.race([]) stays pending, so guard an empty list explicitly.

Race resolved values and return the first input; return null for empty input.

Hint: Handle the empty array before calling race.

```javascript
async function transform(data) {
  if (!data.length) return null;
  return Promise.race(data.map(n => Promise.resolve(n)));
}
```

## 13. Wait for a timer

A timer callback cannot return a value to an already-returned outer function. Wrap the timer in a promise and resolve from its callback. Timer delays are minimum scheduling delays, not precise deadlines.

Wait using a 5 ms timer, then resolve data.value.

Hint: Pass the value to resolve inside setTimeout.

```javascript
async function transform(data) {
  return new Promise(resolve => setTimeout(() => resolve(data.value), 5));
}
```

## 14. Run sequential steps

for...of with await starts the next iteration after the previous one finishes. This is useful for dependencies or limiting pressure on a service. forEach does not await the promises returned by an async callback.

Sequentially await each number and collect cumulative totals.

Hint: Keep a total outside the loop and push after each await.

```javascript
async function transform(data) {
  let total = 0; const totals = [];
  for (const n of data) { total += await Promise.resolve(n); totals.push(total); }
  return totals;
}
```

## 15. Model dependent requests

Some operations require results from earlier ones. Await the dependency before starting the next step. Parallelizing dependent work is not a performance improvement if it uses missing or stale data.

Await the user fixture, then return "profile/" followed by user.id.

Hint: Use the first awaited result to build the second value.

```javascript
async function transform(data) {
  const user = await Promise.resolve(data);
  return await Promise.resolve("profile/" + user.id);
}
```

## 16. Check an HTTP-style response

fetch normally fulfills for HTTP errors such as 404. A production caller checks response.ok before reading the body. This exercise uses a local plain-object fixture, not fetch or a real Response.

If data.ok is true return data.body; otherwise return "HTTP " plus data.status.

Hint: Inspect ok; fulfillment alone does not imply HTTP success.

```javascript
async function transform(data) {
  const response = await Promise.resolve(data);
  if (!response.ok) return "HTTP " + response.status;
  return response.body;
}
```

## 17. Separate parsing failures

JSON.parse can throw even after a request succeeds. Catch parsing errors separately when you want a useful data-quality message. This fixture already contains response text; it performs no network request.

Await data as response text; return parsed JSON, or {error:"Invalid JSON"}.

Hint: Wrap JSON.parse in try/catch.

```javascript
async function transform(data) {
  const text = await Promise.resolve(data);
  try { return JSON.parse(text); } catch { return {error:"Invalid JSON"}; }
}
```

## 18. Bound a retry loop

Retry only failures that are safe and plausibly temporary. Real requests need limits, backoff and cancellation; retrying a payment blindly can duplicate a side effect. This deterministic fixture represents three attempts at most.

Try the first three boolean outcomes in order. Return the 1-based successful attempt, or null.

Hint: Bound the loop and catch each individual rejection.

```javascript
async function transform(data) {
  for (let i = 0; i < Math.min(3, data.length); i++) {
    try { await (data[i] ? Promise.resolve() : Promise.reject(new Error("retry"))); return i + 1; } catch {}
  }
  return null;
}
```

## 19. Ignore stale results

When a user changes a search query, an older result may arrive last. A request ID lets the interface accept only the current response. This suppresses a stale update; it does not cancel the older request.

Await data.response; return its value only when its id equals data.currentId, otherwise null.

Hint: Compare identifiers strictly before using a value.

```javascript
async function transform(data) {
  const response = await Promise.resolve(data.response);
  return response.id === data.currentId ? response.value : null;
}
```

## 20. Build a resilient batch report

Combine allSettled with explicit success and failure records. Preserve identifiers so callers can explain individual failures. This local capstone models independent jobs; passing fixture tests is not proof for every production input.

For records {id,ok,value}, return {id,value} on success or {id,error:"Failed"} on failure, in input order.

Hint: Use each settled result’s index to recover its job ID.

```javascript
async function transform(data) {
  const results = await Promise.allSettled(data.map(async job => { if (!job.ok) throw new Error("Failed"); return job.value; }));
  return results.map((r,i) => r.status === "fulfilled" ? {id:data[i].id,value:r.value} : {id:data[i].id,error:"Failed"});
}
```