Learning Objectives

  • Understand that async functions always return Promises
  • Learn about awaiting non-Promise values
  • Master top-level await
  • Explore various async function patterns

Async Functions Always Return Promises

Every async function automatically wraps its return value in a Promise:

async function getValue() {
  return 42;
}

// Equivalent to:
function getValue() {
  return Promise.resolve(42);
}

getValue().then(value => console.log(value)); // 42

Returning Different Values

// Return a value
async function returnValue() {
  return 'Hello';
}
returnValue(); // Promise { 'Hello' }

// Return a Promise
async function returnPromise() {
  return Promise.resolve('World');
}
returnPromise(); // Promise { 'World' }

// Return nothing (undefined)
async function returnNothing() {
  // No return
}
returnNothing(); // Promise { undefined }

// Throw an error
async function throwError() {
  throw new Error('Oops');
}
throwError(); // Promise {  Error: Oops }

Awaiting Non-Promise Values

You can await any value, not just Promises. Non-Promise values are automatically wrapped:

async function example() {
  const value1 = await 42;           // Wrapped in Promise.resolve()
  const value2 = await 'Hello';      // Wrapped in Promise.resolve()
  const value3 = await [1, 2, 3];    // Wrapped in Promise.resolve()
  
  console.log(value1); // 42
  console.log(value2); // 'Hello'
  console.log(value3); // [1, 2, 3]
}

Async Function Types

1. Function Declaration

async function fetchData() {
  return await fetch('/api/data');
}

2. Function Expression

const fetchData = async function() {
  return await fetch('/api/data');
};

3. Arrow Function

const fetchData = async () => {
  return await fetch('/api/data');
};

// Concise body
const getValue = async () => await fetch('/api/data');

4. Object Method

const api = {
  async fetchData() {
    return await fetch('/api/data');
  },
  
  // Arrow function property
  getData: async () => {
    return await fetch('/api/data');
  }
};

5. Class Method

class API {
  async fetchData() {
    return await fetch('/api/data');
  }
  
  static async getConfig() {
    return await fetch('/api/config');
  }
}

Top-Level Await (ES2022)

In modules, you can use await at the top level without wrapping in an async function:

// In a module file
const data = await fetch('/api/data');
const json = await data.json();

console.log(json);

export default json;

Note: Top-level await only works in ES modules (files with type="module" or .mjs extension).

Top-Level Await Use Cases

// Dynamic imports
const module = await import('./utils.js');

// Resource initialization
const connection = await database.connect();

// Dependency fallbacks
let translations;
try {
  translations = await import(`./i18n/${language}.js`);
} catch {
  translations = await import('./i18n/en.js');
}

Async Function Execution Flow

console.log('1: Before');

async function example() {
  console.log('2: Function start');
  
  const result = await Promise.resolve('data');
  
  console.log('4: After await');
  return result;
}

console.log('3: After function call');
example().then(result => console.log('5: Result:', result));

// Output order:
// 1: Before
// 2: Function start
// 3: After function call
// 4: After await
// 5: Result: data

Async IIFE (Immediately Invoked Function Expression)

// Execute async code immediately
(async () => {
  const data = await fetchData();
  console.log(data);
})();

// With error handling
(async () => {
  try {
    const data = await fetchData();
    console.log(data);
  } catch (error) {
    console.error(error);
  }
})();

Conditional Await

async function getData(useCache) {
  if (useCache) {
    return await getCachedData();
  } else {
    return await fetchFreshData();
  }
}

// Ternary operator
async function getData(useCache) {
  return await (useCache ? getCachedData() : fetchFreshData());
}

Await in Loops

Sequential Execution

async function processItems(items) {
  for (const item of items) {
    await processItem(item); // Waits for each
  }
}

// Using for...of
async function processUsers(userIds) {
  for (const id of userIds) {
    const user = await fetchUser(id);
    console.log(user);
  }
}

Parallel Execution

async function processItems(items) {
  const promises = items.map(item => processItem(item));
  const results = await Promise.all(promises);
  return results;
}

// Or more concisely
async function processItems(items) {
  return await Promise.all(items.map(processItem));
}

Async Generators

async function* generateNumbers() {
  for (let i = 0; i < 5; i++) {
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield i;
  }
}

// Using async generator
(async () => {
  for await (const num of generateNumbers()) {
    console.log(num); // 0, 1, 2, 3, 4 (one per second)
  }
})();

Async Function Composition

async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`);
  return await response.json();
}

async function fetchUserPosts(userId) {
  const response = await fetch(`/api/users/${userId}/posts`);
  return await response.json();
}

async function getUserWithPosts(userId) {
  const user = await fetchUser(userId);
  const posts = await fetchUserPosts(userId);
  return { ...user, posts };
}

// Or in parallel
async function getUserWithPosts(userId) {
  const [user, posts] = await Promise.all([
    fetchUser(userId),
    fetchUserPosts(userId)
  ]);
  return { ...user, posts };
}

Return Await Pattern

// These are different!

// Pattern 1: Return await (in try/catch)
async function pattern1() {
  try {
    return await fetchData(); // Caught by this try/catch
  } catch (error) {
    console.error(error);
    throw error;
  }
}

// Pattern 2: Return without await
async function pattern2() {
  try {
    return fetchData(); // NOT caught by this try/catch!
  } catch (error) {
    console.error(error);
    throw error;
  }
}

Rule: Use return await inside try/catch blocks to ensure errors are caught.

Async Function Performance

// Slow: Sequential (3 seconds total)
async function slow() {
  const a = await delay(1000, 'A');
  const b = await delay(1000, 'B');
  const c = await delay(1000, 'C');
  return [a, b, c];
}

// Fast: Parallel (1 second total)
async function fast() {
  const [a, b, c] = await Promise.all([
    delay(1000, 'A'),
    delay(1000, 'B'),
    delay(1000, 'C')
  ]);
  return [a, b, c];
}

Real-World Example: API Client

class APIClient {
  constructor(baseURL) {
    this.baseURL = baseURL;
  }
  
  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    
    try {
      const response = await fetch(url, options);
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      
      return await response.json();
    } catch (error) {
      console.error(`Request failed: ${endpoint}`, error);
      throw error;
    }
  }
  
  async get(endpoint) {
    return await this.request(endpoint);
  }
  
  async post(endpoint, data) {
    return await this.request(endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
  }
  
  async batchGet(endpoints) {
    return await Promise.all(
      endpoints.map(endpoint => this.get(endpoint))
    );
  }
}

// Usage
const api = new APIClient('https://api.example.com');

(async () => {
  try {
    const [users, posts] = await api.batchGet([
      '/users',
      '/posts'
    ]);
    console.log({ users, posts });
  } catch (error) {
    console.error('Failed to load data:', error);
  }
})();

Key Takeaways

  • ✅ Async functions always return Promises
  • ✅ Can await any value, not just Promises
  • ✅ Top-level await works in ES modules
  • ✅ Use return await in try/catch blocks
  • ✅ Async functions work with all function types
  • ✅ Consider parallel vs sequential execution

Next Steps

Next, we'll explore parallel vs sequential execution patterns in detail!