Blog
Oct 12, 2025 - 12 MIN READ
Understanding Asynchronous Programming with Promises and async/await in JavaScript and TypeScript

Understanding Asynchronous Programming with Promises and async/await in JavaScript and TypeScript

Learn how JavaScript handles asynchronous operations with Promises and async/await — from callbacks to modern async flows, with real examples in both JS and TypeScript.

dogunfx

dogunfx

When you fetch data from an API, read a file, or wait for user input, your program can’t stop everything else from running. JavaScript solves this problem using asynchronous programming, allowing other code to keep running while waiting for slow tasks to finish.

This guide explores Promises and async/await, the modern tools for handling asynchronous code in JavaScript and TypeScript.


1) The Problem: Blocking vs Non-Blocking Code

In most programming languages, operations happen one after another:

// Blocking example
const data = readFileSync("data.txt"); // stops until done
console.log("Finished reading!");

In JavaScript, many APIs are non-blocking — they don’t stop the program.

// Non-blocking example
readFile("data.txt", (err, data) => {
  console.log("Finished reading!");
});
console.log("This runs first!");

Output:

This runs first!
Finished reading!

This asynchronous nature is key to how JavaScript handles I/O.


2) Promises — The Foundation of Async

A Promise represents a value that will be available later.

Creating a Promise

const delay = (ms) =>
  new Promise((resolve) => {
    setTimeout(() => resolve(`Waited ${ms}ms`), ms);
  });

delay(1000).then((msg) => console.log(msg));

Output (after 1s):

Waited 1000ms

Handling Errors

const riskyTask = new Promise((resolve, reject) => {
  const ok = Math.random() > 0.5;
  ok ? resolve("Success!") : reject("Failed!");
});

riskyTask
  .then((msg) => console.log("", msg))
  .catch((err) => console.error("", err))
  .finally(() => console.log("Done."));

Output (randomly):

✅ Success!
Done.

or

❌ Failed!
Done.

3) Chaining Promises

Each .then() returns a new Promise, allowing chaining.

delay(500)
  .then(() => delay(1000))
  .then(() => delay(1500))
  .then(() => console.log("All done!"));

Or shorter:

delay(500)
  .then(() => delay(1000))
  .then(console.log); // logs result of final delay

4) async/await — Cleaner Promise Syntax

async/await is syntactic sugar over Promises, making code look synchronous while remaining non-blocking.

async function run() {
  console.log("Starting...");
  const msg = await delay(1000);
  console.log(msg);
  console.log("Done!");
}

run();

Output:

Starting...
Waited 1000ms
Done!

Error Handling with try/catch

async function riskyRun() {
  try {
    const result = await riskyTask;
    console.log("Result:", result);
  } catch (err) {
    console.error("Caught error:", err);
  }
}

5) Running Multiple Async Tasks

In Sequence

await delay(1000);
await delay(2000);
console.log("3 seconds total");

In Parallel

await Promise.all([delay(1000), delay(2000)]);
console.log("Only 2 seconds total!");

With Promise.race()

const winner = await Promise.race([delay(1000), delay(2000)]);
console.log("Fastest:", winner);

6) Practical Example — Fetching Data

JavaScript

async function fetchTodo() {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  const data = await res.json();
  console.log(data);
}

fetchTodo().catch(console.error);

TypeScript Version

interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

async function fetchTodo(): Promise<Todo> {
  const res = await fetch("https://jsonplaceholder.typicode.com/todos/1");
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<Todo>;
}

fetchTodo()
  .then((todo) => console.log(todo.title))
  .catch(console.error);

7) Combining Async Logic

Example: Fetch Multiple APIs Together

async function fetchMultiple() {
  const urls = [
    "https://jsonplaceholder.typicode.com/todos/1",
    "https://jsonplaceholder.typicode.com/users/1",
  ];
  const [todoRes, userRes] = await Promise.all(urls.map((u) => fetch(u)));
  const [todo, user] = await Promise.all([todoRes.json(), userRes.json()]);
  console.log({ todo, user });
}

fetchMultiple();

Example: Retry Logic

async function fetchWithRetry(url, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      const res = await fetch(url);
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();
    } catch (err) {
      console.warn(`Attempt ${i + 1} failed:`, err.message);
      if (i === retries - 1) throw err;
    }
  }
}

8) Async Patterns in TypeScript

TypeScript makes async code safer with static typing.

async function getData(): Promise<string> {
  return new Promise((resolve) => setTimeout(() => resolve("Done!"), 1000));
}

async function main(): Promise<void> {
  const message = await getData();
  console.log(message);
}

9) Common Mistakes

MistakeWhat HappensFix
Forgetting awaitGets a Promise, not valueUse await myFunc()
Using await in top-level JS (older Node)SyntaxErrorWrap in async IIFE or enable top-level await
Blocking loops (forEach)Doesn’t await async callsUse for...of instead
Forgetting error handlingUncaught rejectionsWrap in try/catch or .catch()

10) Cheat Sheet

FeatureExampleDescription
Create Promisenew Promise((res, rej) => {})Foundation of async ops
Resolve asyncresolve(value)Completes successfully
Reject asyncreject(error)Signals failure
Await Promiseawait myPromisePauses until resolved
Handle errortry { await } catch {}Safe async handling
Run parallelPromise.all([...])Waits for all
Run fastestPromise.race([...])Returns first result

11) References


Asynchronous programming is what makes JavaScript feel fast — the engine doesn’t freeze while waiting for I/O. Once you master Promises and async/await, you’ll be ready to build APIs, work with databases, and handle real-time operations like a pro.

Built by DOGUNFX using Nuxt UI • © 2025