Promises and Async/Await Β· Astro Tech Blog

Promises, Async/Await

Introduction: Callbacks

Callbacks are functions passed as arguments to other functions, to be executed later.

function loadScript(src, callback) {
  let script = document.createElement("script");
  script.src = src;

  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error("Failed to load: " + src));

  document.head.append(script);
}

// Usage
loadScript("script.js", (err, script) => {
  if (err) {
    console.log("Error:", err.message);
  } else {
    console.log("Script loaded:", script.src);
  }
});

Callback Hell

When callbacks are nested, readability suffers:

// ❌ Callback hell / Pyramid of doom
loadScript("a.js", function(err) {
  if (!err) {
    loadScript("b.js", function(err) {
      if (!err) {
        loadScript("c.js", function(err) {
          if (!err) {
            loadScript("d.js", function(err) {
              // ... and so on
            });
          }
        });
      }
    });
  }
});

Promises solve this problem elegantly.

Promise

A Promise represents a value that may be available now, later, or never.

Promise States

  • pending β€” initial state
  • fulfilled β€” operation completed successfully
  • rejected β€” operation failed

Creating a Promise

let promise = new Promise((resolve, reject) => {
  // Async operation
  setTimeout(() => {
    let success = true;

    if (success) {
      resolve("Operation succeeded");
    } else {
      reject(new Error("Operation failed"));
    }
  }, 1000);
});

Consuming a Promise

promise.then(
  (result) => console.log("Success:", result),
  (error) => console.log("Error:", error.message)
);

// Or more clearly:
promise
  .then((result) => console.log("Success:", result))
  .catch((error) => console.log("Error:", error.message))
  .finally(() => console.log("Always runs"));

Example: loadScript with Promise

function loadScript(src) {
  return new Promise((resolve, reject) => {
    let script = document.createElement("script");
    script.src = src;

    script.onload = () => resolve(script);
    script.onerror = () => reject(new Error("Failed: " + src));

    document.head.append(script);
  });
}

loadScript("script.js")
  .then((script) => console.log("Loaded:", script.src))
  .catch((err) => console.log("Error:", err.message));

Immediately Settled Promises

Promise.resolve("Done");      // Creates a fulfilled promise
Promise.reject(new Error("")); // Creates a rejected promise
Demo: Promise Basics
JavaScript
function wait(ms, value) {
return new Promise(resolve => {
setTimeout(() => resolve(value), ms);
});
}

// Chain three async operations
wait(1000, 'First')
.then(result => {
document.write(result + ' (after 1s)<br>');
return wait(1000, 'Second');
})
.then(result => {
document.write(result + ' (after 2s)<br>');
return wait(1000, 'Third');
})
.then(result => {
document.write(result + ' (after 3s)<br>');
document.write('All done!');
});
Live Output Window

Promises Chaining

Each .then() returns a new promise, allowing chaining:

new Promise((resolve) => {
  setTimeout(() => resolve(1), 1000);
})
  .then((result) => {
    console.log(result); // β†’ 1
    return result * 2;
  })
  .then((result) => {
    console.log(result); // β†’ 2
    return result * 2;
  })
  .then((result) => {
    console.log(result); // β†’ 4
    return result * 2;
  })
  .then((result) => {
    console.log(result); // β†’ 8
  });

Returning Promises from .then()

new Promise((resolve) => {
  setTimeout(() => resolve(1), 1000);
})
  .then((result) => {
    console.log(result); // β†’ 1
    return new Promise((resolve) => {
      setTimeout(() => resolve(result * 2), 1000);
    });
  })
  .then((result) => {
    console.log(result); // β†’ 2
    return new Promise((resolve) => {
      setTimeout(() => resolve(result * 2), 1000);
    });
  })
  .then((result) => {
    console.log(result); // β†’ 4
  });

thenable objects

Any object with a .then() method is treated as a promise:

class Thenable {
  constructor(value) {
    this.value = value;
  }
  then(resolve) {
    setTimeout(() => resolve(this.value * 2), 1000);
  }
}

Promise.resolve(5)
  .then(result => new Thenable(result))
  .then(console.log); // β†’ 10 (after 1s)

Error Handling with Promises

.catch() catches errors anywhere in the chain

new Promise((resolve, reject) => {
  throw new Error("Whoops!");
})
  .catch((err) => console.log("Caught:", err.message)); // β†’ "Caught: Whoops!"

// Same as:
new Promise((resolve, reject) => {
  reject(new Error("Whoops!"));
}).catch((err) => console.log("Caught:", err.message));

Implicit try…catch

Promise executor has an implicit try...catch:

new Promise((resolve, reject) => {
  // This throw is caught and turns into a rejection
  throw new Error("Error in executor");
}).catch(console.log);

// Same as:
new Promise((resolve, reject) => {
  reject(new Error("Error in executor"));
}).catch(console.log);

Errors in .then()

Promise.resolve(5)
  .then((result) => {
    throw new Error("Error in then"); // Caught by next catch
  })
  .catch((err) => console.log(err.message));

Rethrowing

If you catch and rethrow, the next .catch() handles it:

Promise.resolve(5)
  .then((result) => {
    throw new Error("First error");
  })
  .catch((err) => {
    console.log("Caught:", err.message);
    throw err; // Rethrow
  })
  .catch((err) => {
    console.log("Caught again:", err.message);
  });
// β†’ "Caught: First error"
// β†’ "Caught again: First error"

Unhandled Rejections

If a promise rejects and there’s no .catch(), it’s an unhandled rejection:

// Browser
window.addEventListener("unhandledrejection", (event) => {
  console.log("Unhandled rejection:", event.reason);
});

// Node.js
process.on("unhandledRejection", (err) => {
  console.error("Unhandled rejection:", err);
});

// Always add .catch() at the end of chains!

Good Error Handling Pattern

function getUser(id) {
  return fetch(`/api/users/${id}`).then(r => r.json());
}

getUser(42)
  .then(user => console.log(user.name))
  .catch(err => {
    console.error("Failed to get user:", err.message);
    // Show user-friendly error message
  });

Promise API

Promise.all

Waits for all promises to fulfill, or rejects immediately if any fails:

let p1 = Promise.resolve(3);
let p2 = new Promise(resolve => setTimeout(() => resolve("done"), 1000));
let p3 = fetch("/api/data").then(r => r.json());

Promise.all([p1, p2, p3]).then((results) => {
  console.log(results); // [3, "done", data] β€” all results in order
});

// If any rejects, Promise.all rejects with that error
Promise.all([
  Promise.resolve(1),
  Promise.reject(new Error("Fail")),
  Promise.resolve(3)
]).catch(err => console.log(err.message)); // β†’ "Fail"

Promise.allSettled (ES2020)

Waits for all promises to settle (fulfilled or rejected):

Promise.allSettled([
  Promise.resolve(1),
  Promise.reject(new Error("Fail")),
  Promise.resolve(3)
]).then((results) => {
  results.forEach((result) => {
    if (result.status === "fulfilled") {
      console.log("Value:", result.value);
    } else {
      console.log("Reason:", result.reason.message);
    }
  });
});
// β†’ "Value: 1"
// β†’ "Reason: Fail"
// β†’ "Value: 3"

Promise.race

Returns the first settled promise (fulfilled or rejected):

let fast = new Promise(resolve => setTimeout(() => resolve("fast"), 500));
let slow = new Promise(resolve => setTimeout(() => resolve("slow"), 2000));

Promise.race([fast, slow]).then(console.log); // β†’ "fast" (after 500ms)

// Useful for timeouts:
let data = Promise.race([
  fetch("/api/data"),
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("Timeout")), 5000)
  )
]);

Promise.any (ES2021)

Returns the first fulfilled promise (ignores rejections):

Promise.any([
  Promise.reject(new Error("Fail 1")),
  Promise.resolve(2),
  Promise.resolve(3)
]).then(console.log); // β†’ 2 (first fulfilled)

// If all reject, returns AggregateError
Promise.any([
  Promise.reject(new Error("Fail 1")),
  Promise.reject(new Error("Fail 2"))
]).catch(err => {
  console.log(err instanceof AggregateError); // β†’ true
  console.log(err.errors); // β†’ [Error, Error]
});

Promise.resolve / Promise.reject

Promise.resolve(42).then(console.log); // β†’ 42
Promise.reject(new Error("Bad")).catch(console.log);
Demo: Promise API Demo
JavaScript
function delay(ms, value) {
return new Promise(resolve => setTimeout(() => resolve(value), ms));
}

// Promise.all
Promise.all([
delay(1000, 'Task 1 (1s)'),
delay(500, 'Task 2 (0.5s)'),
delay(1500, 'Task 3 (1.5s)')
]).then(results => {
document.write('Promise.all results:<br>');
results.forEach(r => document.write('- ' + r + '<br>'));
});

// Promise.race
Promise.race([
delay(2000, 'Slow'),
delay(1000, 'Fast')
]).then(result => {
document.write('<br>Promise.race winner: ' + result + '<br>');
});

// Promise.any
Promise.any([
Promise.reject(new Error('Fails')),
delay(800, 'First success')
]).then(result => {
document.write('<br>Promise.any result: ' + result + '<br>');
});
Live Output Window

Promisification

Converting a callback-based function to return a promise:

// Callback-based
function readFile(path, encoding, callback) {
  // ... reads file, calls callback(err, data)
}

// Promisified
function readFilePromise(path, encoding) {
  return new Promise((resolve, reject) => {
    readFile(path, encoding, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}

// Usage
readFilePromise("/path/to/file", "utf-8")
  .then((data) => console.log(data))
  .catch((err) => console.error(err));

General Promisification Function

function promisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn.call(this, ...args, (err, result) => {
        if (err) reject(err);
        else resolve(result);
      });
    });
  };
}

let readFileAsync = promisify(readFile);
let fs = require("fs");
let readFile = promisify(fs.readFile);

// Node.js has built-in promisify
const { promisify } = require("util");
let readFile = promisify(require("fs").readFile);

Microtasks

Event Loop

JavaScript has an event loop that processes:

  1. Macrotasks β€” setTimeout, setInterval, I/O, UI rendering
  2. Microtasks β€” Promise.then/catch/finally, queueMicrotask, MutationObserver
console.log("Start");

setTimeout(() => console.log("Timeout"), 0); // Macrotask

Promise.resolve()
  .then(() => console.log("Promise 1")) // Microtask
  .then(() => console.log("Promise 2")); // Microtask

console.log("End");

// Output:
// β†’ "Start"
// β†’ "End"
// β†’ "Promise 1"
// β†’ "Promise 2"
// β†’ "Timeout"

Microtasks Queue

Microtasks are processed before the next macrotask:

setTimeout(() => console.log("Timeout 1"), 0);
setTimeout(() => console.log("Timeout 2"), 0);

Promise.resolve().then(() => {
  console.log("Promise");
  // This adds another microtask
  Promise.resolve().then(() => console.log("Promise nested"));
});

console.log("Synchronous");

// β†’ "Synchronous"
// β†’ "Promise"
// β†’ "Promise nested"
// β†’ "Timeout 1"
// β†’ "Timeout 2"

queueMicrotask

queueMicrotask(() => {
  console.log("This runs as a microtask");
});

Why This Matters

Promise handlers run before DOM rendering and before setTimeout callbacks:

// DOM updates happen after microtasks
document.getElementById("loading").textContent = "Loading...";

fetch("/api/data")
  .then(data => {
    // Microtask β€” runs before rendering
    // DOM isn't updated yet from the line above
  });

Async/Await

async/await makes asynchronous code look synchronous.

async function

An async function always returns a promise:

async function greet() {
  return "Hello";
}

let result = greet();
console.log(result); // β†’ Promise {<fulfilled>: "Hello"}

// Equivalent to:
function greet() {
  return Promise.resolve("Hello");
}

await

await pauses execution until the promise settles:

async function getData() {
  let response = await fetch("/api/data");
  let data = await response.json();
  console.log(data);
  return data;
}

// The function is paused at each await
// Other code can run during the wait

Async/await vs Promise Chains

// Promise chain
function getWeather(city) {
  return fetch(`/api/weather/${city}`)
    .then(response => response.json())
    .then(data => data.temperature)
    .catch(err => console.error("Failed:", err));
}

// Async/await β€” much cleaner
async function getWeather(city) {
  try {
    let response = await fetch(`/api/weather/${city}`);
    let data = await response.json();
    return data.temperature;
  } catch (err) {
    console.error("Failed:", err);
  }
}

Sequential vs Parallel

// Sequential (one after another)
async function sequential() {
  let user = await getUser(1);
  let posts = await getPosts(user.id);
  let comments = await getComments(posts[0].id);
  return comments;
}

// Parallel (all at once)
async function parallel() {
  let [user, posts, comments] = await Promise.all([
    getUser(1),
    getPosts(1),
    getComments(1)
  ]);
  return { user, posts, comments };
}

Error Handling with Async/Await

async function processUser(id) {
  try {
    let user = await fetch(`/api/users/${id}`);
    return await user.json();
  } catch (err) {
    console.error("Error:", err.message);
    throw err; // Re-throw if needed
  }
}

// Or use .catch() on the returned promise
processUser(42).catch(err => console.error("Handled outside:", err));

Async/Await with Loops

// Sequential processing with for...of
async function processItems(items) {
  for (let item of items) {
    await process(item); // One at a time
  }
}

// Parallel processing with map
async function processItems(items) {
  await Promise.all(items.map(item => process(item))); // All at once
}

// Important: forEach doesn't work with await!
// items.forEach(async (item) => { await process(item); }); // ❌

Top-level await (Modules)

In ES modules, await works at the top level:

<script type="module">
  let response = await fetch("/api/data");
  let data = await response.json();
  console.log(data);
</script>
// In ES module files
import { fetch } from "./api.js";

let config = await fetch("/api/config").then(r => r.json());
export default config;
Demo: Async/Await Demo
JavaScript
// Simulated async functions
function fetchUser(id) {
return new Promise(resolve => {
setTimeout(() => resolve({ id, name: 'User ' + id }), 1000);
});
}

function fetchPosts(userId) {
return new Promise(resolve => {
setTimeout(() => resolve(['Post 1', 'Post 2', 'Post 3']), 800);
});
}

// Using async/await
async function loadUserProfile(userId) {
try {
document.write('Loading user...<br>');
let user = await fetchUser(userId);
document.write('User: ' + user.name + '<br>');

document.write('Loading posts...<br>');
let posts = await fetchPosts(user.id);
document.write('Posts: ' + posts.join(', ') + '<br>');

document.write('Profile loaded successfully!');
} catch (err) {
document.write('Error: ' + err.message);
}
}

loadUserProfile(42);
Live Output Window