Introduction to Async Functions
Async functions represent one of the most significant improvements to JavaScript's handling of asynchronous operations. Introduced in ES2017, async functions provide a more elegant and readable way to work with Promises, making asynchronous code almost as straightforward as synchronous code.
At their core, async functions are a syntactic feature built on top of Promises, offering a more intuitive way to handle asynchronous operations without sacrificing the power and flexibility of the Promise model.
The Anatomy of an Async Function
An async function is declared using the async keyword before the function declaration. This signals that the function will work with asynchronous operations and will automatically return a Promise.
// Basic syntax of an async function
async function fetchUserData(userId) {
// Asynchronous operations go here
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
return userData; // This value will be wrapped in a Promise
}
// Async function expression
const fetchUserData = async function(userId) {
// Function body
};
// Async arrow function
const fetchUserData = async (userId) => {
// Function body
};
// Async method in a class or object
class UserService {
async getUserData(userId) {
// Method body
}
}
Key Characteristics
- Always returns a Promise: Even if you return a non-Promise value, it gets automatically wrapped in a Promise.
- Can use await: Only within async functions can you use the
awaitkeyword. - Maintain chain-ability: Since they return Promises, async functions can be chained with
.then()and.catch().
Comparison: Promises vs Async/Await
Let's compare the traditional Promise approach with the async/await syntax to understand how async functions improve code readability and maintainability.
// Using Promises with .then()/.catch()
function fetchUserDataPromise(userId) {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error('User not found');
}
return response.json();
})
.then(userData => {
return fetch(`/api/posts?userId=${userData.id}`);
})
.then(response => response.json())
.then(posts => {
return {
user: userData,
posts: posts
};
})
.catch(error => {
console.error('Failed to fetch user data:', error);
throw error;
});
}
// Using async/await
async function fetchUserDataAsync(userId) {
try {
const userResponse = await fetch(`/api/users/${userId}`);
if (!userResponse.ok) {
throw new Error('User not found');
}
const userData = await userResponse.json();
const postsResponse = await fetch(`/api/posts?userId=${userData.id}`);
const posts = await postsResponse.json();
return {
user: userData,
posts: posts
};
} catch (error) {
console.error('Failed to fetch user data:', error);
throw error;
}
}
Benefits of Async/Await
- More readable: Code flows top-to-bottom like synchronous code.
- Better error handling: Use familiar try/catch blocks instead of .catch() chains.
- Cleaner variable scoping: Variables are accessible throughout the function, no need for nested closures.
- Easier debugging: Stack traces are more meaningful, and you can set breakpoints more intuitively.
Real-World Applications
Data Fetching in Web Applications
Async functions excel at handling HTTP requests, which are one of the most common asynchronous operations in web development.
// Weather app example
async function getWeatherForLocation(city) {
try {
// First, get coordinates for the city
const geoResponse = await fetch(`https://geocoding-api.example.com/search?q=${encodeURIComponent(city)}`);
const geoData = await geoResponse.json();
if (!geoData.results || geoData.results.length === 0) {
throw new Error(`City not found: ${city}`);
}
const { lat, lon } = geoData.results[0];
// Then, get weather data for those coordinates
const weatherResponse = await fetch(`https://weather-api.example.com/forecast?lat=${lat}&lon=${lon}`);
const weatherData = await weatherResponse.json();
return {
city: city,
current: weatherData.current,
forecast: weatherData.daily.slice(0, 5), // 5-day forecast
lastUpdated: new Date().toISOString()
};
} catch (error) {
console.error(`Weather fetch failed for ${city}:`, error);
throw new Error(`Could not get weather for ${city}: ${error.message}`);
}
}
Database Operations
Database queries are often asynchronous, making async functions a natural fit for database interactions.
// Node.js example with MongoDB
async function createUserOrder(userId, items) {
const session = await mongoose.startSession();
session.startTransaction();
try {
// Get user and check eligibility
const user = await User.findById(userId).session(session);
if (!user) throw new Error('User not found');
if (user.accountStatus !== 'active') {
throw new Error('User account is not active');
}
// Calculate order total
let total = 0;
for (const item of items) {
const product = await Product.findById(item.productId).session(session);
if (!product) throw new Error(`Product not found: ${item.productId}`);
// Check inventory
if (product.stockQuantity < item.quantity) {
throw new Error(`Not enough inventory for ${product.name}`);
}
// Update product inventory
product.stockQuantity -= item.quantity;
await product.save({ session });
total += product.price * item.quantity;
}
// Create the order
const order = new Order({
user: userId,
items: items,
total: total,
status: 'pending'
});
await order.save({ session });
// Commit the transaction
await session.commitTransaction();
session.endSession();
return order;
} catch (error) {
// Abort transaction on error
await session.abortTransaction();
session.endSession();
console.error('Order creation failed:', error);
throw error;
}
}
Animation and Timing Functions
Async functions can simplify complex timing and animation sequences that would otherwise require nested callbacks or complex Promise chains.
// Animation sequence with timing
async function animateElementSequence(element) {
// Helper function for controlled timing
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
try {
// Fade out
element.style.transition = 'opacity 500ms ease-out';
element.style.opacity = '0';
await wait(500); // Wait for transition to complete
// Move to new position
element.style.opacity = '0';
element.style.transition = 'transform 800ms cubic-bezier(0.1, 0.7, 1.0, 0.1)';
element.style.transform = 'translateX(200px)';
await wait(800);
// Change color
element.style.transition = 'background-color 300ms ease-in';
element.style.backgroundColor = '#3f51b5';
await wait(300);
// Fade back in
element.style.transition = 'opacity 500ms ease-in';
element.style.opacity = '1';
await wait(500);
return 'Animation sequence completed';
} catch (error) {
console.error('Animation failed:', error);
// Reset the element
element.style.transition = '';
element.style.opacity = '1';
element.style.transform = '';
}
}
Under the Hood: How Async Functions Work
To truly master async functions, it's valuable to understand what's happening behind the scenes. Async functions are essentially syntactic sugar over Promises and generators.
When JavaScript encounters an async function:
- It creates a Promise that will eventually resolve to the function's return value.
- The function executes until it hits an
awaitexpression. - The
awaitpauses the function's execution and waits for the Promise to settle. - Once the awaited Promise settles, the function continues execution from that point.
- If the awaited Promise rejects, an exception is thrown at the await line, which can be caught with try/catch.
- When the function completes, the return value resolves the Promise created in step 1.
This implementation uses JavaScript generators under the hood to create pausable functions, but the async/await syntax makes this whole process transparent to developers.
// What the compiler roughly transforms async/await into:
function fetchUserData(userId) {
// Create a Promise to return
return new Promise((resolve, reject) => {
// Create a generator-based state machine
const generator = function* () {
try {
// First await: fetch user
const userResponse = yield fetch(`/api/users/${userId}`);
if (!userResponse.ok) {
throw new Error('User not found');
}
// Second await: parse JSON
const userData = yield userResponse.json();
// Third await: fetch posts
const postsResponse = yield fetch(`/api/posts?userId=${userData.id}`);
// Fourth await: parse JSON
const posts = yield postsResponse.json();
// Return value (resolves the Promise)
return {
user: userData,
posts: posts
};
} catch (error) {
console.error('Failed to fetch user data:', error);
throw error;
}
}();
// Runner function to process the generator
function run(genObj) {
function step(value) {
let result;
try {
result = genObj.next(value);
} catch (error) {
return reject(error);
}
if (result.done) {
return resolve(result.value);
}
// Handle the yielded Promise
Promise.resolve(result.value)
.then(value => step(value))
.catch(error => {
try {
result = genObj.throw(error);
if (result.done) {
return resolve(result.value);
}
step(result.value);
} catch (error) {
return reject(error);
}
});
}
step();
}
// Start the generator runner
run(generator);
});
}
While you don't need to understand this implementation detail to use async/await effectively, knowing what's happening behind the scenes can help you debug tricky issues and better understand the execution flow.
Advanced Async Function Patterns
Concurrent Operations with await
A common misconception is that async/await forces everything to be sequential. However, you can still run operations concurrently by starting Promises before awaiting them.
// Sequential execution (slower)
async function fetchSequential() {
const start = Date.now();
const userData = await fetchUser();
const productData = await fetchProducts();
const orderData = await fetchOrders();
console.log(`Sequential fetch took ${Date.now() - start}ms`);
return { userData, productData, orderData };
}
// Concurrent execution (faster)
async function fetchConcurrent() {
const start = Date.now();
// Start all fetches immediately (they run in parallel)
const userPromise = fetchUser();
const productPromise = fetchProducts();
const orderPromise = fetchOrders();
// Now await their results
const userData = await userPromise;
const productData = await productPromise;
const orderData = await orderPromise;
console.log(`Concurrent fetch took ${Date.now() - start}ms`);
return { userData, productData, orderData };
}
// Even better approach using Promise.all
async function fetchWithPromiseAll() {
const start = Date.now();
const [userData, productData, orderData] = await Promise.all([
fetchUser(),
fetchProducts(),
fetchOrders()
]);
console.log(`Promise.all fetch took ${Date.now() - start}ms`);
return { userData, productData, orderData };
}
Self-Executing Async Functions (IIFE)
When you need async/await at the top level where async functions are not allowed, you can use an immediately invoked async function.
// Top-level async code with IIFE
(async function() {
try {
const config = await fetchAppConfig();
initializeApp(config);
const user = await authenticateUser();
renderUserDashboard(user);
} catch (error) {
displayErrorScreen(error);
}
})();
// Modern alternative: top-level await in ES modules
// Only works in ES modules and modern environments
const config = await fetchAppConfig();
initializeApp(config);
try {
const user = await authenticateUser();
renderUserDashboard(user);
} catch (error) {
displayErrorScreen(error);
}
Async Iteration
For asynchronous iteration over data sources, JavaScript provides for await...of loops within async functions.
// Processing a stream of data asynchronously
async function processLogEntries() {
const logSource = getLogStreamSource();
try {
// Iterate over an async iterable
for await (const entry of logSource) {
// Each iteration waits for the next entry
await analyzeLogEntry(entry);
updateDashboard(entry);
}
console.log('All log entries processed');
} catch (error) {
console.error('Error processing logs:', error);
}
}
// Example implementation of an async generator
async function* fetchPaginatedData(endpoint, pageSize = 100) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${endpoint}?page=${page}&limit=${pageSize}`);
const data = await response.json();
if (data.items.length === 0) {
hasMore = false;
} else {
// Yield each page of results
yield data.items;
page++;
}
}
}
// Using the async generator
async function downloadAllCustomers() {
const allCustomers = [];
// Process each page as it arrives
for await (const customerPage of fetchPaginatedData('/api/customers')) {
for (const customer of customerPage) {
allCustomers.push(customer);
updateProgress(allCustomers.length);
}
}
return allCustomers;
}
Error Handling in Async Functions
One of the major advantages of async/await is the return to familiar try/catch error handling. However, there are some nuances to be aware of.
resolves?"} C -->|"Yes"| D["Continue execution"] C -->|"No"| E["Jump to catch block"] E --> F["catch (error) { ... }"] F --> G["Error handled"] style A fill:#e3f2fd,stroke:#333,stroke-width:1px style B fill:#e8f5e9,stroke:#333,stroke-width:1px style C fill:#fff3e0,stroke:#333,stroke-width:1px style F fill:#ffebee,stroke:#333,stroke-width:1px
// Basic error handling
async function processUserData(userId) {
try {
const user = await fetchUser(userId);
const processedData = await processData(user);
return processedData;
} catch (error) {
// This catches errors from both fetchUser and processData
console.error('Processing failed:', error);
throw new Error(`Failed to process user ${userId}: ${error.message}`);
}
}
// Selective error handling
async function processUserDataSelective(userId) {
try {
// First operation
const user = await fetchUser(userId);
try {
// Second operation with isolated error handling
const posts = await fetchUserPosts(userId);
user.posts = posts;
} catch (postError) {
// Only catch errors from fetchUserPosts
console.warn('Could not fetch user posts:', postError);
user.posts = []; // Provide a fallback
}
try {
// Third operation with isolated error handling
const analytics = await fetchUserAnalytics(userId);
user.analytics = analytics;
} catch (analyticsError) {
// Only catch errors from fetchUserAnalytics
console.warn('Could not fetch user analytics:', analyticsError);
user.analytics = { visits: 0, actions: 0 }; // Fallback
}
return user;
} catch (error) {
// This mainly catches errors from fetchUser
throw new Error(`User processing failed: ${error.message}`);
}
}
Common Error Handling Patterns
// Retry pattern with exponential backoff
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let retries = 0;
while (true) {
try {
return await fetch(url, options);
} catch (error) {
retries++;
if (retries >= maxRetries) {
console.error(`Failed after ${maxRetries} retries:`, error);
throw error;
}
// Exponential backoff with jitter
const delay = Math.min(1000 * Math.pow(2, retries), 10000)
+ Math.floor(Math.random() * 1000);
console.warn(`Retry ${retries} after ${delay}ms`);
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Fallback pattern
async function getUserWithFallback(userId) {
try {
// Try primary source
return await fetchFromMainDB(userId);
} catch (mainError) {
console.warn('Main DB error, trying cache:', mainError);
try {
// Try cache as fallback
return await fetchFromCache(userId);
} catch (cacheError) {
console.warn('Cache error, using default:', cacheError);
// Return a default when all else fails
return {
id: userId,
name: 'Unknown User',
isDefault: true
};
}
}
}
// Finally pattern for cleanup
async function processFileWithCleanup(filePath) {
let fileHandle = null;
try {
// Acquire resource
fileHandle = await fs.promises.open(filePath, 'r');
const content = await fileHandle.readFile('utf8');
return processContent(content);
} catch (error) {
console.error('File processing error:', error);
throw error;
} finally {
// Cleanup will run regardless of success or failure
if (fileHandle) {
try {
await fileHandle.close();
console.log('File handle closed');
} catch (closeError) {
console.warn('Error closing file:', closeError);
}
}
}
}
Performance Considerations
While async/await provides excellent readability, it's important to understand its performance implications and how to optimize asynchronous code.
Potential Performance Pitfalls
- Accidental Sequentialization: Writing await statements in sequence when operations could run concurrently
- Unnecessary async Wrappers: Creating async functions that don't need to be async
- Promise Creation Overhead: Creating many small async functions when not necessary
// Performance anti-pattern: Sequential awaits when concurrency is possible
async function fetchAllUserData(userIds) {
const users = [];
// BAD: Sequential processing when we could do this concurrently
for (const id of userIds) {
const user = await fetchUser(id); // Each call waits for the previous to complete
users.push(user);
}
return users;
}
// Better approach: Use Promise.all for concurrency
async function fetchAllUserDataConcurrent(userIds) {
// Create all promises at once
const userPromises = userIds.map(id => fetchUser(id));
// Wait for all to complete
const users = await Promise.all(userPromises);
return users;
}
// Performance anti-pattern: Too many small async functions
// This creates unnecessary Promise overhead
const getValue = async () => 42; // Unnecessary async
const double = async (x) => x * 2; // Unnecessary async
async function compute() {
const value = await getValue(); // Unnecessary await
return await double(value); // Unnecessary await
}
// Better approach: Only use async when needed
const getValue = () => 42; // Synchronous
const double = (x) => x * 2; // Synchronous
async function compute() {
// Only the truly asynchronous parts need async/await
const value = getValue();
const result = double(value);
await saveToDatabase(result); // This is actually async
return result;
}
Balancing Readability and Performance
When optimizing async code, it's important to balance performance with readability and maintainability.
// Helper function to make concurrent code more readable
async function fetchConcurrent(fetchFunctions) {
const results = await Promise.all(fetchFunctions.map(fn => fn()));
return results;
}
// Using the helper
async function loadDashboard(userId) {
// Concurrent fetching with clear intent
const [user, posts, comments, analytics] = await fetchConcurrent([
() => fetchUser(userId),
() => fetchPosts(userId),
() => fetchComments(userId),
() => fetchAnalytics(userId)
]);
return {
user,
posts,
comments,
analytics
};
}
Async Functions in Different Environments
Browser Environment
Modern browsers fully support async/await, but polyfills may be needed for older browsers.
// Browser event listeners with async functions
document.getElementById('fetch-button').addEventListener('click', async (event) => {
try {
const resultElement = document.getElementById('result');
resultElement.textContent = 'Loading...';
const data = await fetchData();
resultElement.textContent = JSON.stringify(data, null, 2);
} catch (error) {
document.getElementById('error').textContent = `Error: ${error.message}`;
}
});
// Service workers with async functions
self.addEventListener('fetch', event => {
event.respondWith(handleFetch(event.request));
});
async function handleFetch(request) {
// Try to get from cache first
const cache = await caches.open('v1');
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
// If not in cache, fetch from network
try {
const response = await fetch(request);
// Clone the response to store in cache and return
const responseClone = response.clone();
await cache.put(request, responseClone);
return response;
} catch (error) {
return new Response('Network error', { status: 503 });
}
}
Node.js Environment
Node.js has excellent support for async/await, which works well with Node's event-driven architecture.
// Express middleware with async functions
app.get('/api/users/:id', async (req, res, next) => {
try {
const userId = req.params.id;
const user = await User.findById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
// Pass errors to Express error handler
next(error);
}
});
// Async middleware wrapper to avoid try/catch repetition
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Using the wrapper
app.get('/api/products', asyncHandler(async (req, res) => {
const products = await Product.find({});
res.json(products);
// No try/catch needed, errors will be caught by the wrapper
}));
// Promisified filesystem operations
const fs = require('fs').promises;
async function processConfigFile(path) {
// Read and parse config file
const configData = await fs.readFile(path, 'utf8');
const config = JSON.parse(configData);
// Modify config
config.lastUpdated = new Date().toISOString();
// Write back to file
await fs.writeFile(path, JSON.stringify(config, null, 2));
return config;
}
Testing Async Functions
Testing asynchronous code requires special considerations. Modern testing frameworks provide good support for async/await patterns.
// Jest example
describe('User Service', () => {
test('should fetch user by ID', async () => {
// Given
const userId = '123';
const mockUser = { id: userId, name: 'Test User' };
// Mock the fetch function
global.fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockUser)
})
);
// When
const userService = new UserService();
const result = await userService.getUserById(userId);
// Then
expect(result).toEqual(mockUser);
expect(fetch).toHaveBeenCalledWith(`/api/users/${userId}`);
});
test('should handle errors correctly', async () => {
// Given
const errorMessage = 'Network Error';
// Mock fetch to throw an error
global.fetch = jest.fn().mockImplementation(() =>
Promise.reject(new Error(errorMessage))
);
// When & Then
const userService = new UserService();
await expect(userService.getUserById('123')).rejects.toThrow(errorMessage);
});
});
// Mocha example
describe('File Processor', function() {
it('should process files correctly', async function() {
// Increase timeout for async test
this.timeout(5000);
// Setup test environment
const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'test-'));
const testFile = path.join(tempDir, 'test.txt');
await fs.promises.writeFile(testFile, 'Test content');
try {
// Run the async function under test
const result = await processFile(testFile);
// Assertions
assert.strictEqual(result.processed, true);
assert.strictEqual(result.size, 12);
// Check file was modified
const content = await fs.promises.readFile(testFile, 'utf8');
assert.strictEqual(content, 'PROCESSED: Test content');
} finally {
// Clean up
await fs.promises.rm(tempDir, { recursive: true });
}
});
});
Best Practices and Common Patterns
Best Practices
- Always handle errors: Use try/catch blocks to handle potential exceptions.
- Consider using Promise.all for concurrent operations: Start Promises in parallel when order doesn't matter.
- Return early: In async functions, return early to avoid unnecessary nesting.
- Use await only when necessary: Don't await synchronous operations.
- Be careful with loops: Consider whether iterations should be sequential or concurrent.
- Keep functions focused: Async functions should do one thing well, like their synchronous counterparts.
Common Patterns
// Pattern: Async function wrapper
// Converts callback-based APIs to Promise-based
function readFileAsync(path, options) {
return new Promise((resolve, reject) => {
fs.readFile(path, options, (error, data) => {
if (error) {
reject(error);
} else {
resolve(data);
}
});
});
}
// Pattern: Sequential processing with early returns
async function validateUser(user) {
// Early return for invalid input
if (!user || !user.id) {
return { valid: false, reason: 'Invalid user object' };
}
try {
// Check if user exists in database
const dbUser = await fetchUserFromDb(user.id);
if (!dbUser) {
return { valid: false, reason: 'User not found in database' };
}
// Check if user is active
if (dbUser.status !== 'active') {
return { valid: false, reason: 'User account is not active' };
}
// Check if user has required permissions
const permissions = await fetchUserPermissions(user.id);
if (!permissions.includes('read')) {
return { valid: false, reason: 'User lacks required permissions' };
}
// All checks passed
return { valid: true, user: dbUser };
} catch (error) {
return { valid: false, reason: `Validation error: ${error.message}` };
}
}
// Pattern: Dependency injection for testability
class UserService {
constructor(fetchClient = fetch) {
this.fetchClient = fetchClient;
}
async getUserById(id) {
const response = await this.fetchClient(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.statusText}`);
}
return response.json();
}
}
// For testing, inject a mock fetch client
const mockFetch = jest.fn();
const testService = new UserService(mockFetch);
Common Mistakes to Avoid
// Mistake: Forgetting to await
async function updateUser(userId, data) {
try {
// Missing await - returns a Promise, but doesn't wait for it!
const result = saveToDatabase(userId, data);
// This will execute before the save completes!
console.log('User updated successfully');
return { success: true };
} catch (error) {
// This catch block will never execute for saveToDatabase errors
console.error('Failed to update user:', error);
return { success: false, error: error.message };
}
}
// Correct version
async function updateUser(userId, data) {
try {
// With await - properly waits for completion
const result = await saveToDatabase(userId, data);
console.log('User updated successfully');
return { success: true };
} catch (error) {
console.error('Failed to update user:', error);
return { success: false, error: error.message };
}
}
// Mistake: Unnecessary Promise chains with async/await
async function processData(data) {
// Mixing Promise chains with async/await - confusing!
return await fetchUserById(data.userId)
.then(user => {
return processUserData(user);
})
.then(async (processed) => {
// More awaits inside .then()
const result = await saveProcessedData(processed);
return result;
});
}
// Cleaner version with consistent async/await
async function processData(data) {
// Logical flow is much clearer
const user = await fetchUserById(data.userId);
const processed = await processUserData(user);
const result = await saveProcessedData(processed);
return result;
}
Practice Exercises
Exercise 1: Convert Promise Chains to Async/Await
Take the following Promise-based function and rewrite it using async/await:
function fetchUserAndPosts(userId) {
return fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error('User not found');
}
return response.json();
})
.then(user => {
return fetch(`/api/posts?userId=${user.id}`)
.then(response => response.json())
.then(posts => {
user.posts = posts;
return user;
});
})
.catch(error => {
console.error('Error fetching user data:', error);
throw error;
});
}
Exercise 2: Implement Concurrent Operations
Create an async function that loads data from multiple sources concurrently and combines the results.
Exercise 3: Error Handling and Recovery
Implement an async function that tries to fetch data from a primary source and falls back to a secondary source if the primary fails.
Exercise 4: Async Iteration
Create an async generator function that yields paginated results from an API, and a consumer function that processes each page of results.
Summary
Async functions represent a major advancement in JavaScript's handling of asynchronous operations. Key points to remember:
- Async functions always return Promises, even when returning synchronous values.
- The
awaitkeyword can only be used inside async functions (or top-level modules with modern JavaScript). - Await pauses execution until the Promise resolves, making asynchronous code read like synchronous code.
- Error handling can use familiar try/catch blocks instead of Promise catch chains.
- To run operations concurrently, start the Promises before awaiting their results or use Promise combinators like Promise.all().
- Async functions work in all modern browsers and Node.js environments.
By mastering async functions, you gain the ability to write asynchronous code that is cleaner, more maintainable, and easier to reason about, while still leveraging the full power of JavaScript's Promise-based concurrency model.