JavaScript Promises Tutorial - Section 4: Async/Await
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
// 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 }
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 fetchData() {
return await fetch('/api/data');
}
const fetchData = async function() {
return await fetch('/api/data');
};
const fetchData = async () => {
return await fetch('/api/data');
};
// Concise body
const getValue = async () => await fetch('/api/data');
const api = {
async fetchData() {
return await fetch('/api/data');
},
// Arrow function property
getData: async () => {
return await fetch('/api/data');
}
};
class API {
async fetchData() {
return await fetch('/api/data');
}
static async getConfig() {
return await fetch('/api/config');
}
}
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).
// 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');
}
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
// 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);
}
})();
async function getData(useCache) {
if (useCache) {
return await getCachedData();
} else {
return await fetchFreshData();
}
}
// Ternary operator
async function getData(useCache) {
return await (useCache ? getCachedData() : fetchFreshData());
}
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);
}
}
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 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 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 };
}
// 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.
// 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];
}
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);
}
})();
return await in try/catch blocksNext, we'll explore parallel vs sequential execution patterns in detail!