JavaScript is an asynchronous programming language, which means it can handle multiple operations at the same time without blocking the main thread. When working with asynchronous operations like API calls, file reading, or database queries, you have two main approaches: Promises and Async/Await.
In this article, you will learn the differences between these two approaches, when to use each one, and how to make the right choice for your specific use case.
Here’s what we’ll cover:
What Are Asynchronous Operations?
Before explaining what Promises and Async/Await mean, it is important to understand what asynchronous operations are.
Synchronous operations execute one after another, blocking the next operation until the current one completes. Here’s an example in JavaScript:
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"First"</span>);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Second"</span>);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Third"</span>);
<span class="hljs-comment">// Output:</span>
<span class="hljs-comment">// First</span>
<span class="hljs-comment">// Second</span>
<span class="hljs-comment">// Third</span>
Asynchronous operations, on the other hand, can start an operation and continue executing other code while waiting for the first operation to complete. Here’s an example in JavaScript:
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"First"</span>);
<span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Second (after 2 seconds)"</span>);
}, <span class="hljs-number">2000</span>);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Third"</span>);
<span class="hljs-comment">// Output:</span>
<span class="hljs-comment">// First</span>
<span class="hljs-comment">// Third</span>
<span class="hljs-comment">// Second (after 2 seconds)</span>
In this example, setTimeout()
is an asynchronous function that schedules code to run after a specified delay without blocking the execution of subsequent code.
What Are Promises?
A Promise is a JavaScript object that represents the eventual completion (or failure) of an asynchronous operation. Think of it as a placeholder for a value that will be available in the future.
Promise States
A Promise can be in one of three states:
-
Pending: The initial state – the operation hasn’t been completed yet
-
Fulfilled (Resolved): The operation completed successfully
-
Rejected: The operation failed
Basic Promise Syntax
Here’s how you create and use a basic Promise:
<span class="hljs-comment">// Creating a Promise</span>
<span class="hljs-keyword">const</span> myPromise = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =></span> {
<span class="hljs-comment">// Simulate an asynchronous operation</span>
<span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =></span> {
<span class="hljs-keyword">const</span> success = <span class="hljs-literal">true</span>;
<span class="hljs-keyword">if</span> (success) {
resolve(<span class="hljs-string">"Operation completed successfully!"</span>);
} <span class="hljs-keyword">else</span> {
reject(<span class="hljs-string">"Operation failed!"</span>);
}
}, <span class="hljs-number">2000</span>);
});
<span class="hljs-comment">// Using the Promise</span>
myPromise
.then(<span class="hljs-function">(<span class="hljs-params">result</span>) =></span> {
<span class="hljs-built_in">console</span>.log(result); <span class="hljs-comment">// "Operation completed successfully!"</span>
})
.catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =></span> {
<span class="hljs-built_in">console</span>.log(error);
});
Let’s break down this code:
-
new Promise()
creates a new Promise object -
The Promise constructor takes a function with two parameters:
resolve
andreject
-
resolve()
is called when the operation succeeds -
reject()
is called when the operation fails -
.then()
handles the successful case -
.catch()
handles the error case
Chaining Promises
Promise chaining is a powerful technique that allows you to link multiple asynchronous operations together in a sequence. When you want to perform multiple operations where each depends on the result of the previous one, promise chaining provides an elegant solution. You can chain multiple Promises together using .then()
:
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchUserData</span>(<span class="hljs-params">userId</span>) </span>{
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =></span> {
<span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =></span> {
resolve({ <span class="hljs-attr">id</span>: userId, <span class="hljs-attr">name</span>: <span class="hljs-string">"John Doe"</span> });
}, <span class="hljs-number">1000</span>);
});
}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchUserPosts</span>(<span class="hljs-params">user</span>) </span>{
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =></span> {
<span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =></span> {
resolve([
{ <span class="hljs-attr">title</span>: <span class="hljs-string">"Post 1"</span>, <span class="hljs-attr">author</span>: user.name },
{ <span class="hljs-attr">title</span>: <span class="hljs-string">"Post 2"</span>, <span class="hljs-attr">author</span>: user.name }
]);
}, <span class="hljs-number">1000</span>);
});
}
<span class="hljs-comment">// Chaining Promises</span>
fetchUserData(<span class="hljs-number">123</span>)
.then(<span class="hljs-function">(<span class="hljs-params">user</span>) =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"User:"</span>, user);
<span class="hljs-keyword">return</span> fetchUserPosts(user);
})
.then(<span class="hljs-function">(<span class="hljs-params">posts</span>) =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Posts:"</span>, posts);
})
.catch(<span class="hljs-function">(<span class="hljs-params">error</span>) =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Error:"</span>, error);
});
In this example:
-
fetchUserData()
returns a Promise that resolves with user information -
fetchUserPosts()
returns a Promise that resolves with the user’s posts -
We chain these operations using
.then()
-
Each
.then()
receives the resolved value from the previous Promise
Downsides of Promise Chaining:
While promise chaining is powerful, it does have some potential drawbacks:
-
“Callback Hell” in disguise: Complex chains can become difficult to read and debug, especially with nested logic
-
Complex error handling: Each step in the chain needs proper error handling, and errors can propagate in unexpected ways
-
Debugging challenges: Stack traces through promise chains can be harder to follow
-
Mixing synchronous and asynchronous logic: It can be tempting to put synchronous operations inside .then() blocks, which can lead to confusion
What Is Async/Await?
Async/Await is syntactic sugar built on top of Promises. It allows you to write asynchronous code that looks and behaves more like synchronous code, making it easier to read and understand.
Basic Async/Await Syntax
Here’s the same Promise example rewritten using Async/Await:
<span class="hljs-comment">// Creating an async function</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">performOperation</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> myPromise;
<span class="hljs-built_in">console</span>.log(result); <span class="hljs-comment">// "Operation completed successfully!"</span>
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-built_in">console</span>.log(error);
}
}
performOperation();
Let’s break down this code:
-
The
async
keyword before a function declaration makes it an asynchronous function -
The
await
keyword pauses the function execution until the Promise resolves -
The
try/catch
blocks handle errors, similar to.catch()
in Promises
Converting Promise Chains to Async/Await
Here’s the previous chaining example using Async/Await:
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getUserDataAndPosts</span>(<span class="hljs-params">userId</span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> fetchUserData(userId);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"User:"</span>, user);
<span class="hljs-keyword">const</span> posts = <span class="hljs-keyword">await</span> fetchUserPosts(user);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Posts:"</span>, posts);
<span class="hljs-keyword">return</span> posts;
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Error:"</span>, error);
<span class="hljs-keyword">throw</span> error; <span class="hljs-comment">// Re-throw the error if needed</span>
}
}
getUserDataAndPosts(<span class="hljs-number">123</span>);
This code is much more readable and follows a linear flow that’s easier to understand.
Practical Examples: Promises vs Async/Await
Let’s compare both approaches with real-world scenarios.
Example 1: Making API Calls
Using Promises:
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchDataWithPromises</span>(<span class="hljs-params"></span>) </span>{
fetch(<span class="hljs-string">'https://jsonplaceholder.typicode.com/users/1'</span>)
.then(<span class="hljs-function"><span class="hljs-params">response</span> =></span> {
<span class="hljs-keyword">if</span> (!response.ok) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Network response was not ok'</span>);
}
<span class="hljs-keyword">return</span> response.json();
})
.then(<span class="hljs-function"><span class="hljs-params">user</span> =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'User data:'</span>, user);
<span class="hljs-keyword">return</span> fetch(<span class="hljs-string">`https://jsonplaceholder.typicode.com/users/<span class="hljs-subst">${user.id}</span>/posts`</span>);
})
.then(<span class="hljs-function"><span class="hljs-params">response</span> =></span> response.json())
.then(<span class="hljs-function"><span class="hljs-params">posts</span> =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'User posts:'</span>, posts);
})
.catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error:'</span>, error);
});
}
Using Async/Await:
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchDataWithAsyncAwait</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> userResponse = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'https://jsonplaceholder.typicode.com/users/1'</span>);
<span class="hljs-keyword">if</span> (!userResponse.ok) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Network response was not ok'</span>);
}
<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> userResponse.json();
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'User data:'</span>, user);
<span class="hljs-keyword">const</span> postsResponse = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`https://jsonplaceholder.typicode.com/users/<span class="hljs-subst">${user.id}</span>/posts`</span>);
<span class="hljs-keyword">const</span> posts = <span class="hljs-keyword">await</span> postsResponse.json();
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'User posts:'</span>, posts);
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error:'</span>, error);
}
}
The Async/Await version is more readable and follows a natural top-to-bottom flow.
Example 2: Handling Multiple Asynchronous Operations
Using Promises:
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processMultipleOperations</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> promise1 = fetch(<span class="hljs-string">'https://jsonplaceholder.typicode.com/users/1'</span>);
<span class="hljs-keyword">const</span> promise2 = fetch(<span class="hljs-string">'https://jsonplaceholder.typicode.com/users/2'</span>);
<span class="hljs-keyword">const</span> promise3 = fetch(<span class="hljs-string">'https://jsonplaceholder.typicode.com/users/3'</span>);
<span class="hljs-built_in">Promise</span>.all([promise1, promise2, promise3])
.then(<span class="hljs-function"><span class="hljs-params">responses</span> =></span> {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.all(responses.map(<span class="hljs-function"><span class="hljs-params">response</span> =></span> response.json()));
})
.then(<span class="hljs-function"><span class="hljs-params">users</span> =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'All users:'</span>, users);
})
.catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error:'</span>, error);
});
}
Using Async/Await:
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processMultipleOperationsAsync</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> promise1 = fetch(<span class="hljs-string">'https://jsonplaceholder.typicode.com/users/1'</span>);
<span class="hljs-keyword">const</span> promise2 = fetch(<span class="hljs-string">'https://jsonplaceholder.typicode.com/users/2'</span>);
<span class="hljs-keyword">const</span> promise3 = fetch(<span class="hljs-string">'https://jsonplaceholder.typicode.com/users/3'</span>);
<span class="hljs-keyword">const</span> responses = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([promise1, promise2, promise3]);
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(responses.map(<span class="hljs-function"><span class="hljs-params">response</span> =></span> response.json()));
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'All users:'</span>, users);
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error:'</span>, error);
}
}
Both approaches use Promise. all()
to wait for multiple operations to complete simultaneously.
When to Use Promises
Promises are still useful in several scenarios:
1. Working with Existing Promise-Based APIs
Popular libraries like Axios, fetch(), and many Node.js modules return Promises.
How to identify promise-based APIs:
-
The function returns an object with
.then()
and.catch()
methods -
The documentation mentions “returns a Promise”
-
The function doesn’t require a callback parameter
Many libraries and APIs return Promises directly:
<span class="hljs-comment">// Axios library returns Promises</span>
axios.get(<span class="hljs-string">'/api/users'</span>)
.then(<span class="hljs-function"><span class="hljs-params">response</span> =></span> response.data)
.then(<span class="hljs-function"><span class="hljs-params">users</span> =></span> <span class="hljs-built_in">console</span>.log(users))
.catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> <span class="hljs-built_in">console</span>.error(error));
<span class="hljs-comment">// fetch() API returns Promises</span>
fetch(<span class="hljs-string">'/api/data'</span>)
.then(<span class="hljs-function"><span class="hljs-params">response</span> =></span> response.json())
.then(<span class="hljs-function"><span class="hljs-params">data</span> =></span> <span class="hljs-built_in">console</span>.log(data));
<span class="hljs-comment">// Node.js fs.promises returns Promises</span>
<span class="hljs-keyword">import</span> { readFile } <span class="hljs-keyword">from</span> <span class="hljs-string">'fs/promises'</span>;
readFile(<span class="hljs-string">'./config.json'</span>, <span class="hljs-string">'utf8'</span>)
.then(<span class="hljs-function"><span class="hljs-params">data</span> =></span> <span class="hljs-built_in">JSON</span>.parse(data))
.then(<span class="hljs-function"><span class="hljs-params">config</span> =></span> <span class="hljs-built_in">console</span>.log(config));
2. Functional Programming Patterns
Promises are immutable objects that represent future values, making them perfect for functional programming approaches. They can be easily composed, chained, and transformed without side effects. The .then()
method essentially maps over the future value, similar to how Array.map()
works with collections.
Promises work well with functional programming approaches because they are composable and can be easily passed around as first-class objects:
<span class="hljs-comment">// Functional composition with Promises</span>
<span class="hljs-keyword">const</span> processUsers = <span class="hljs-function">(<span class="hljs-params">userIds</span>) =></span> {
<span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.all(
userIds.map(<span class="hljs-function"><span class="hljs-params">id</span> =></span> fetchUser(id)) <span class="hljs-comment">// Transform each ID to a Promise</span>
)
.then(<span class="hljs-function"><span class="hljs-params">users</span> =></span> users.filter(<span class="hljs-function"><span class="hljs-params">user</span> =></span> user.active)) <span class="hljs-comment">// Filter active users</span>
.then(<span class="hljs-function"><span class="hljs-params">activeUsers</span> =></span> activeUsers.map(<span class="hljs-function"><span class="hljs-params">user</span> =></span> user.email)); <span class="hljs-comment">// Extract emails</span>
};
<span class="hljs-comment">// Pipeline approach</span>
<span class="hljs-keyword">const</span> createUserPipeline = <span class="hljs-function">(<span class="hljs-params">userId</span>) =></span> {
<span class="hljs-keyword">return</span> fetchUser(userId)
.then(validateUser)
.then(enrichUserData)
.then(formatUserResponse)
.then(logUserActivity);
};
<span class="hljs-comment">// Composing multiple Promise-returning functions</span>
<span class="hljs-keyword">const</span> compose = <span class="hljs-function">(<span class="hljs-params">...fns</span>) =></span> <span class="hljs-function">(<span class="hljs-params">value</span>) =></span>
fns.reduce(<span class="hljs-function">(<span class="hljs-params">promise, fn</span>) =></span> promise.then(fn), <span class="hljs-built_in">Promise</span>.resolve(value));
<span class="hljs-keyword">const</span> userProcessor = compose(
fetchUser,
validateUser,
enrichUserData,
saveUser
);
3. Creating Reusable Promise Utilities
Reusable promise utilities are helper functions that abstract common asynchronous patterns into reusable components. They’re particularly useful for cross-cutting concerns like retries, timeouts, rate limiting, and caching. These utilities can be used across different parts of your application without being tied to specific business logic.
When they’re useful:
-
When you need the same asynchronous pattern in multiple places
-
For handling common failure scenarios (network timeouts, retries)
-
When building middleware or interceptors
-
For performance optimizations like batching or debouncing
<span class="hljs-comment">// Timeout utility</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">timeout</span>(<span class="hljs-params">ms</span>) </span>{
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function"><span class="hljs-params">resolve</span> =></span> <span class="hljs-built_in">setTimeout</span>(resolve, ms));
}
<span class="hljs-comment">// Retry utility with exponential backoff</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">retry</span>(<span class="hljs-params">fn, retries = <span class="hljs-number">3</span>, delay = <span class="hljs-number">1000</span></span>) </span>{
<span class="hljs-keyword">return</span> fn().catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> {
<span class="hljs-keyword">if</span> (retries > <span class="hljs-number">0</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Retrying... <span class="hljs-subst">${retries}</span> attempts left`</span>);
<span class="hljs-keyword">return</span> timeout(delay).then(<span class="hljs-function">() =></span> retry(fn, retries - <span class="hljs-number">1</span>, delay * <span class="hljs-number">2</span>));
}
<span class="hljs-keyword">throw</span> error;
});
}
<span class="hljs-comment">// Rate limiting utility</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">rateLimit</span>(<span class="hljs-params">fn, maxCalls, timeWindow</span>) </span>{
<span class="hljs-keyword">let</span> calls = [];
<span class="hljs-keyword">return</span> <span class="hljs-function"><span class="hljs-keyword">function</span>(<span class="hljs-params">...args</span>) </span>{
<span class="hljs-keyword">const</span> now = <span class="hljs-built_in">Date</span>.now();
calls = calls.filter(<span class="hljs-function"><span class="hljs-params">time</span> =></span> now - time < timeWindow);
<span class="hljs-keyword">if</span> (calls.length >= maxCalls) {
<span class="hljs-keyword">const</span> waitTime = timeWindow - (now - calls[<span class="hljs-number">0</span>]);
<span class="hljs-keyword">return</span> timeout(waitTime).then(<span class="hljs-function">() =></span> fn.apply(<span class="hljs-built_in">this</span>, args));
}
calls.push(now);
<span class="hljs-keyword">return</span> fn.apply(<span class="hljs-built_in">this</span>, args);
};
}
<span class="hljs-comment">// Usage examples</span>
<span class="hljs-keyword">const</span> apiCall = <span class="hljs-function">() =></span> fetch(<span class="hljs-string">'/api/data'</span>).then(<span class="hljs-function"><span class="hljs-params">r</span> =></span> r.json());
<span class="hljs-keyword">const</span> resilientApiCall = retry(apiCall, <span class="hljs-number">3</span>);
<span class="hljs-keyword">const</span> rateLimitedApiCall = rateLimit(apiCall, <span class="hljs-number">5</span>, <span class="hljs-number">60000</span>); <span class="hljs-comment">// 5 calls per minute</span>
When to Use Async/Await
Async/Await is preferred in most modern JavaScript applications. It has various advantages over Promises, such as:
-
Improved readability: Code reads like synchronous code, making it easier to understand the flow
-
Better debugging: Stack traces are cleaner and easier to follow
-
Simplified error handling: Single try/catch block can handle multiple async operations
-
Reduced nesting: Eliminates the “pyramid of doom” that can occur with promise chains
-
Easier testing: Async functions are easier to test and mock
-
Better IDE support: Better autocomplete and type inference in modern editors
Let’s look at some examples that demonstrate when async/await would be a better choice.
1. Sequential Operations
When you need to perform operations one after another:
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processUserData</span>(<span class="hljs-params">userId</span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> fetchUser(userId);
<span class="hljs-keyword">const</span> preferences = <span class="hljs-keyword">await</span> fetchUserPreferences(user.id);
<span class="hljs-keyword">const</span> recommendations = <span class="hljs-keyword">await</span> generateRecommendations(user, preferences);
<span class="hljs-keyword">return</span> {
user,
preferences,
recommendations
};
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to process user data:'</span>, error);
<span class="hljs-keyword">throw</span> error;
}
}
Why this is better than promises: With promise chaining, you’d need to nest .then() calls or return values through the chain, making it harder to track data flow.
2. Complex Error Handling
Async/await allows you to use familiar try/catch syntax and handle errors at the exact point where they might occur. You can have multiple try/catch blocks for different error scenarios, and the error handling logic is co-located with the code that might throw the error.
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">complexOperation</span>(<span class="hljs-params">data</span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-comment">// First level: preprocessing errors</span>
<span class="hljs-keyword">const</span> processedData = <span class="hljs-keyword">await</span> preprocessData(data);
<span class="hljs-keyword">try</span> {
<span class="hljs-comment">// Second level: critical operation errors</span>
<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> performCriticalOperation(processedData);
<span class="hljs-keyword">return</span> result;
} <span class="hljs-keyword">catch</span> (criticalError) {
<span class="hljs-comment">// Handle critical operation errors specifically</span>
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Critical operation failed:'</span>, criticalError);
<span class="hljs-comment">// We can make decisions based on the error type</span>
<span class="hljs-keyword">if</span> (criticalError.code === <span class="hljs-string">'TEMPORARY_FAILURE'</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Attempting fallback operation...'</span>);
<span class="hljs-keyword">const</span> fallbackResult = <span class="hljs-keyword">await</span> performFallbackOperation(processedData);
<span class="hljs-keyword">return</span> fallbackResult;
} <span class="hljs-keyword">else</span> {
<span class="hljs-comment">// Re-throw if it's not recoverable</span>
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Critical failure: <span class="hljs-subst">${criticalError.message}</span>`</span>);
}
}
} <span class="hljs-keyword">catch</span> (preprocessError) {
<span class="hljs-comment">// Handle preprocessing errors differently</span>
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Preprocessing failed:'</span>, preprocessError);
<span class="hljs-comment">// We can inspect the error and decide how to handle it</span>
<span class="hljs-keyword">if</span> (preprocessError.code === <span class="hljs-string">'INVALID_DATA'</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Invalid input data provided'</span>);
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Unable to process data'</span>);
}
}
}
Explanation of the code:
-
The outer try/catch handles preprocessing errors
-
The inner try/catch specifically handles critical operation errors
-
Each error handler can make different decisions based on error types
-
The code clearly shows the error-handling strategy at each level
-
You can easily add logging, metrics, or recovery logic at each level
3. Conditional Asynchronous Logic
Async/await makes it natural to use standard control flow (if/else, loops, switch statements) with asynchronous operations. This is much cleaner than trying to implement conditional logic within promise chains.
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">smartUserProcess</span>(<span class="hljs-params">userId</span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-comment">// First, get the user data</span>
<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> fetchUser(userId);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Processing user: <span class="hljs-subst">${user.name}</span> (Premium: <span class="hljs-subst">${user.isPremium}</span>)`</span>);
<span class="hljs-comment">// Make decisions based on the async result</span>
<span class="hljs-keyword">if</span> (user.isPremium) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'User is premium - fetching premium features'</span>);
<span class="hljs-comment">// Premium users get additional data</span>
<span class="hljs-keyword">const</span> premiumData = <span class="hljs-keyword">await</span> fetchPremiumFeatures(user.id);
<span class="hljs-comment">// We can make further decisions based on premium data</span>
<span class="hljs-keyword">if</span> (premiumData.analyticsEnabled) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Analytics enabled - generating premium analytics'</span>);
<span class="hljs-keyword">const</span> analytics = <span class="hljs-keyword">await</span> generatePremiumAnalytics(user, premiumData);
<span class="hljs-keyword">return</span> { user, premiumData, analytics };
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">return</span> { user, premiumData };
}
} <span class="hljs-keyword">else</span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'User is basic - fetching basic features'</span>);
<span class="hljs-comment">// Basic users get different treatment</span>
<span class="hljs-keyword">const</span> basicData = <span class="hljs-keyword">await</span> fetchBasicFeatures(user.id);
<span class="hljs-comment">// Check if user qualifies for upgrade prompts</span>
<span class="hljs-keyword">if</span> (basicData.usageLevel > <span class="hljs-number">0.8</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'User has high usage - checking upgrade eligibility'</span>);
<span class="hljs-keyword">const</span> upgradeOffer = <span class="hljs-keyword">await</span> checkUpgradeEligibility(user);
<span class="hljs-keyword">return</span> { user, basicData, upgradeOffer };
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">return</span> { user, basicData };
}
}
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'User processing failed:'</span>, error);
<span class="hljs-comment">// Even error handling can be conditional</span>
<span class="hljs-keyword">if</span> (error.code === <span class="hljs-string">'USER_NOT_FOUND'</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'User does not exist'</span>);
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (error.code === <span class="hljs-string">'NETWORK_ERROR'</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Network connectivity issue'</span>);
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">throw</span> error;
}
}
}
Explanation of the code:
-
We await the user fetch and immediately use the result in an if statement
-
Each branch of the conditional can perform different async operations
-
We can nest conditions naturally (like checking analyticsEnabled)
-
Standard control flow works seamlessly with async operations
-
Error handling can also be conditional based on error types
-
The code reads like synchronous code but handles async operations correctly
Performance Considerations
Understanding the performance implications of different asynchronous patterns is crucial for building efficient JavaScript applications. The main performance consideration is whether your asynchronous operations can run in parallel or must be executed sequentially.
When working with multiple asynchronous operations, you have two main execution patterns: sequential (one after another) and parallel (multiple operations at the same time). The choice between these patterns can significantly impact your application’s performance and user experience.
Sequential vs Parallel Execution
Sequential Execution (slower but sometimes necessary):
Sequential execution means waiting for each operation to complete before starting the next one. This is slower but necessary when operations depend on each other.
<span class="hljs-comment">// This takes ~3 seconds total (1 + 1 + 1)</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sequentialOperations</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-built_in">console</span>.time(<span class="hljs-string">'Sequential Operations'</span>);
<span class="hljs-keyword">const</span> result1 = <span class="hljs-keyword">await</span> operation1(); <span class="hljs-comment">// 1 second - must complete first</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Operation 1 completed:'</span>, result1);
<span class="hljs-keyword">const</span> result2 = <span class="hljs-keyword">await</span> operation2(); <span class="hljs-comment">// 1 second - starts after operation1</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Operation 2 completed:'</span>, result2);
<span class="hljs-keyword">const</span> result3 = <span class="hljs-keyword">await</span> operation3(); <span class="hljs-comment">// 1 second - starts after operation2</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Operation 3 completed:'</span>, result3);
<span class="hljs-built_in">console</span>.timeEnd(<span class="hljs-string">'Sequential Operations'</span>);
<span class="hljs-keyword">return</span> [result1, result2, result3];
}
Use sequential execution when:
-
Each operation depends on the result of the previous one
-
You need to process results in a specific order
-
Operations must be rate-limited (for example, API calls with rate limits)
-
You want to avoid overwhelming external services
Parallel Execution (faster when possible):
Parallel execution means starting all operations at the same time and waiting for all of them to complete. This is much faster when operations are independent.
<span class="hljs-comment">// This takes ~1 second total (all operations run simultaneously)</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">parallelOperations</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-built_in">console</span>.time(<span class="hljs-string">'Parallel Operations'</span>);
<span class="hljs-comment">// Start all operations immediately - they run concurrently</span>
<span class="hljs-keyword">const</span> promise1 = operation1(); <span class="hljs-comment">// starts immediately</span>
<span class="hljs-keyword">const</span> promise2 = operation2(); <span class="hljs-comment">// starts immediately </span>
<span class="hljs-keyword">const</span> promise3 = operation3(); <span class="hljs-comment">// starts immediately</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'All operations started, waiting for completion...'</span>);
<span class="hljs-comment">// Wait for all operations to complete</span>
<span class="hljs-keyword">const</span> [result1, result2, result3] = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
promise1,
promise2,
promise3
]);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'All operations completed'</span>);
<span class="hljs-built_in">console</span>.timeEnd(<span class="hljs-string">'Parallel Operations'</span>);
<span class="hljs-keyword">return</span> [result1, result2, result3];
}
Use parallel execution when:
-
Operations are independent of each other
-
You want to minimize total execution time
-
Dealing with I/O operations (file reads, API calls, database queries)
-
The order of completion doesn’t matter
Advanced Example – Mixed Approach:
Sometimes you need a combination of both approaches:
javascriptasync <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mixedApproach</span>(<span class="hljs-params">userIds</span>) </span>{
<span class="hljs-built_in">console</span>.time(<span class="hljs-string">'Mixed Approach'</span>);
<span class="hljs-comment">// Step 1: Fetch all users in parallel (they're independent)</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Fetching users in parallel...'</span>);
<span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all(
userIds.map(<span class="hljs-function"><span class="hljs-params">id</span> =></span> fetchUser(id))
);
<span class="hljs-comment">// Step 2: Process each user sequentially (to avoid overwhelming the recommendation service)</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Processing users sequentially...'</span>);
<span class="hljs-keyword">const</span> results = [];
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> user <span class="hljs-keyword">of</span> users) {
<span class="hljs-keyword">const</span> preferences = <span class="hljs-keyword">await</span> fetchUserPreferences(user.id);
<span class="hljs-keyword">const</span> recommendations = <span class="hljs-keyword">await</span> generateRecommendations(user, preferences);
results.push({ user, preferences, recommendations });
<span class="hljs-comment">// Small delay to be respectful to the API</span>
<span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function"><span class="hljs-params">resolve</span> =></span> <span class="hljs-built_in">setTimeout</span>(resolve, <span class="hljs-number">100</span>));
}
<span class="hljs-built_in">console</span>.timeEnd(<span class="hljs-string">'Mixed Approach'</span>);
<span class="hljs-keyword">return</span> results;
}
Use Promise.all()
when operations can run independently and simultaneously.
Error Handling Patterns
Proper error handling is crucial for building robust applications. Different asynchronous patterns offer different approaches to error handling, each with their own advantages and use cases.
Error handling in asynchronous JavaScript can be challenging because errors can occur at different points in the execution flow. Understanding how to properly catch, handle, and recover from errors in both Promise and async/await patterns is essential for building reliable applications.
Promise Error Handling:
With Promises, errors are handled using the .catch()
method. This approach provides fine-grained control over error handling at different points in the chain.
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">promiseErrorHandling</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">return</span> fetchData()
.then(<span class="hljs-function"><span class="hljs-params">data</span> =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Data fetched successfully'</span>);
<span class="hljs-keyword">return</span> processData(data);
})
.then(<span class="hljs-function"><span class="hljs-params">result</span> =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Data processed successfully'</span>);
<span class="hljs-keyword">return</span> saveResult(result);
})
.then(<span class="hljs-function"><span class="hljs-params">savedResult</span> =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Result saved successfully'</span>);
<span class="hljs-keyword">return</span> savedResult;
})
.catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error occurred in the chain:'</span>, error);
<span class="hljs-comment">// Handle different types of errors</span>
<span class="hljs-keyword">if</span> (error.name === <span class="hljs-string">'NetworkError'</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Network issue detected, attempting retry...'</span>);
<span class="hljs-keyword">return</span> retryOperation();
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (error.name === <span class="hljs-string">'ValidationError'</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Data validation failed:'</span>, error.message);
<span class="hljs-keyword">return</span> handleValidationError(error);
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (error.name === <span class="hljs-string">'StorageError'</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Storage operation failed, using fallback'</span>);
<span class="hljs-keyword">return</span> saveToFallbackStorage(error.data);
} <span class="hljs-keyword">else</span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Unknown error type:'</span>, error);
<span class="hljs-keyword">throw</span> error; <span class="hljs-comment">// Re-throw if we can't handle it</span>
}
});
}
Here’s what’s going on in this code:
-
The
.catch()
method catches any error that occurs in the entire chain -
You can inspect the error object to determine the appropriate response
-
Returning a value from
.catch()
recovers from the error -
Throwing an error or not returning anything propagates the error further
-
The error handler has access to the original error context
Async/Await Error Handling
With async/await, errors are handled using try/catch blocks, which provide more familiar and flexible error-handling patterns.
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">asyncAwaitErrorHandling</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Starting data processing...'</span>);
<span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> fetchData();
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Data fetched successfully'</span>);
<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> processData(data);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Data processed successfully'</span>);
<span class="hljs-keyword">const</span> savedResult = <span class="hljs-keyword">await</span> saveResult(result);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Result saved successfully'</span>);
<span class="hljs-keyword">return</span> savedResult;
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error occurred during processing:'</span>, error);
<span class="hljs-comment">// Handle different types of errors with more complex logic</span>
<span class="hljs-keyword">if</span> (error.name === <span class="hljs-string">'NetworkError'</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Network issue detected'</span>);
<span class="hljs-comment">// We can use async operations in error handling</span>
<span class="hljs-keyword">const</span> retryCount = <span class="hljs-keyword">await</span> getRetryCount();
<span class="hljs-keyword">if</span> (retryCount < <span class="hljs-number">3</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Retrying... (attempt <span class="hljs-subst">${retryCount + <span class="hljs-number">1</span>}</span>)`</span>);
<span class="hljs-keyword">await</span> incrementRetryCount();
<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> retryOperation();
} <span class="hljs-keyword">else</span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Max retries reached, switching to offline mode'</span>);
<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> switchToOfflineMode();
}
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (error.name === <span class="hljs-string">'ValidationError'</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Data validation failed:'</span>, error.message);
<span class="hljs-comment">// Handle validation errors with user feedback</span>
<span class="hljs-keyword">await</span> logValidationError(error);
<span class="hljs-keyword">return</span> handleValidationError(error);
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (error.name === <span class="hljs-string">'StorageError'</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Storage operation failed'</span>);
<span class="hljs-comment">// Try multiple fallback options</span>
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> saveToFallbackStorage(error.data);
} <span class="hljs-keyword">catch</span> (fallbackError) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Fallback storage also failed, using cache'</span>);
<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> saveToCache(error.data);
}
} <span class="hljs-keyword">else</span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Unknown error type, logging for analysis'</span>);
<span class="hljs-keyword">await</span> logErrorForAnalysis(error);
<span class="hljs-keyword">throw</span> error; <span class="hljs-comment">// Re-throw unknown errors</span>
}
}
}
Here’s what’s going on in the code:
-
The try/catch block handles all errors in the async function
-
You can use await inside catch blocks for error recovery operations
-
Multiple nested try/catch blocks can handle different scenarios
-
Error handling code can be as complex as needed, including loops and conditions
-
The error handling logic is co-located with the code that might throw
Advanced Error Pattern – Specific Error Handling:
javascriptasync <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">advancedErrorHandling</span>(<span class="hljs-params">userId</span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> fetchUser(userId);
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> sensitiveData = <span class="hljs-keyword">await</span> fetchSensitiveData(user.id);
<span class="hljs-keyword">return</span> { user, sensitiveData };
} <span class="hljs-keyword">catch</span> (sensitiveError) {
<span class="hljs-comment">// Handle sensitive data errors specifically</span>
<span class="hljs-keyword">if</span> (sensitiveError.code === <span class="hljs-string">'PERMISSION_DENIED'</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'User lacks permission for sensitive data'</span>);
<span class="hljs-keyword">return</span> { user, <span class="hljs-attr">sensitiveData</span>: <span class="hljs-literal">null</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'permission_denied'</span> };
} <span class="hljs-keyword">else</span> {
<span class="hljs-comment">// For other sensitive data errors, we still want to return the user</span>
<span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'Sensitive data unavailable:'</span>, sensitiveError.message);
<span class="hljs-keyword">return</span> { user, <span class="hljs-attr">sensitiveData</span>: <span class="hljs-literal">null</span>, <span class="hljs-attr">reason</span>: <span class="hljs-string">'data_unavailable'</span> };
}
}
} <span class="hljs-keyword">catch</span> (userError) {
<span class="hljs-comment">// Handle user fetching errors</span>
<span class="hljs-keyword">if</span> (userError.code === <span class="hljs-string">'USER_NOT_FOUND'</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`User <span class="hljs-subst">${userId}</span> does not exist`</span>);
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (userError.code === <span class="hljs-string">'NETWORK_ERROR'</span>) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Unable to connect to user service'</span>);
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Failed to fetch user: <span class="hljs-subst">${userError.message}</span>`</span>);
}
}
}
Best Practices
Following these best practices will help you write more reliable, maintainable, and performant asynchronous JavaScript code.
Always Handle Errors
One of the most common mistakes in asynchronous JavaScript is forgetting to handle errors. When an async operation fails without proper error handling, it can lead to unhandled promise rejections, application crashes, or silent failures that are difficult to debug.
Don’t do this:
javascript<span class="hljs-comment">// Missing error handling - this is dangerous!</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">badExample</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> fetchData(); <span class="hljs-comment">// What if this fails?</span>
<span class="hljs-keyword">const</span> processed = <span class="hljs-keyword">await</span> processData(data); <span class="hljs-comment">// What if this fails?</span>
<span class="hljs-keyword">return</span> <span class="hljs-keyword">await</span> saveData(processed); <span class="hljs-comment">// What if this fails?</span>
}
<span class="hljs-comment">// Usage - user has no idea if something went wrong</span>
<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> badExample();
<span class="hljs-built_in">console</span>.log(result); <span class="hljs-comment">// Could be undefined or cause a crash</span>
Why this is problematic:
-
If
fetchData()
fails, the entire function crashes -
The caller has no way to know what went wrong
-
In production, this could lead to a poor user experience
-
Debugging becomes much harder without proper error context
Do this instead:
javascript<span class="hljs-comment">// Proper error handling with context and recovery</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">goodExample</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Starting data processing...'</span>);
<span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> fetchData();
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Data fetched successfully'</span>);
<span class="hljs-keyword">const</span> processed = <span class="hljs-keyword">await</span> processData(data);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Data processed successfully'</span>);
<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> saveData(processed);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Data saved successfully'</span>);
<span class="hljs-keyword">return</span> result;
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-comment">// Log the error with context</span>
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Data processing failed:'</span>, {
<span class="hljs-attr">error</span>: error.message,
<span class="hljs-attr">step</span>: error.step || <span class="hljs-string">'unknown'</span>,
<span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString()
});
<span class="hljs-comment">// Re-throw with more context or handle appropriately</span>
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Data processing failed: <span class="hljs-subst">${error.message}</span>`</span>);
}
}
<span class="hljs-comment">// Usage - caller knows how to handle failures</span>
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> goodExample();
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Success:'</span>, result);
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to process data:'</span>, error.message);
<span class="hljs-comment">// Handle the error appropriately for your application</span>
showErrorToUser(<span class="hljs-string">'Data processing failed. Please try again.'</span>);
}
Why this is better:
-
Every async operation is wrapped in try/catch
-
Errors are logged with useful context
-
The caller receives meaningful error messages
-
The application can gracefully handle failures
Use Promise.all()
for Independent Operations
A common performance anti-pattern is making independent async operations run sequentially when they could run in parallel. This unnecessarily increases the total execution time.
Don’t do this:
javascript<span class="hljs-comment">// Sequential when it could be parallel - this is inefficient!</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">inefficient</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-built_in">console</span>.time(<span class="hljs-string">'Inefficient Approach'</span>);
<span class="hljs-comment">// These operations are independent but run one after another</span>
<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> fetchUser(); <span class="hljs-comment">// 500ms</span>
<span class="hljs-keyword">const</span> posts = <span class="hljs-keyword">await</span> fetchPosts(); <span class="hljs-comment">// 300ms </span>
<span class="hljs-keyword">const</span> comments = <span class="hljs-keyword">await</span> fetchComments(); <span class="hljs-comment">// 400ms</span>
<span class="hljs-comment">// Total time: ~1200ms</span>
<span class="hljs-built_in">console</span>.timeEnd(<span class="hljs-string">'Inefficient Approach'</span>);
<span class="hljs-keyword">return</span> { user, posts, comments };
}
Why this is problematic:
-
Each operation waits for the previous one to complete
-
Total execution time is the sum of all individual times
-
Network resources are underutilized
-
Users experience unnecessary delays
This is particularly common when fetching data for a dashboard or page that needs multiple pieces of information. Developers often write the code sequentially without considering that these operations can run simultaneously.
Do this instead:
javascript<span class="hljs-comment">// Parallel execution - much more efficient!</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">efficient</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-built_in">console</span>.time(<span class="hljs-string">'Efficient Approach'</span>);
<span class="hljs-comment">// Start all operations simultaneously</span>
<span class="hljs-keyword">const</span> userPromise = fetchUser(); <span class="hljs-comment">// starts immediately</span>
<span class="hljs-keyword">const</span> postsPromise = fetchPosts(); <span class="hljs-comment">// starts immediately</span>
<span class="hljs-keyword">const</span> commentsPromise = fetchComments(); <span class="hljs-comment">// starts immediately</span>
<span class="hljs-comment">// Wait for all to complete</span>
<span class="hljs-keyword">const</span> [user, posts, comments] = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.all([
userPromise,
postsPromise,
commentsPromise
]);
<span class="hljs-comment">// Total time: ~500ms (longest individual operation)</span>
<span class="hljs-built_in">console</span>.timeEnd(<span class="hljs-string">'Efficient Approach'</span>);
<span class="hljs-keyword">return</span> { user, posts, comments };
}
Advanced example with error handling:
javascriptasync <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">efficientWithErrorHandling</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Starting parallel data fetch...'</span>);
<span class="hljs-comment">// Start all operations and handle individual failures</span>
<span class="hljs-keyword">const</span> results = <span class="hljs-keyword">await</span> <span class="hljs-built_in">Promise</span>.allSettled([
fetchUser().catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> ({ <span class="hljs-attr">error</span>: error.message, <span class="hljs-attr">type</span>: <span class="hljs-string">'user'</span> })),
fetchPosts().catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> ({ <span class="hljs-attr">error</span>: error.message, <span class="hljs-attr">type</span>: <span class="hljs-string">'posts'</span> })),
fetchComments().catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> ({ <span class="hljs-attr">error</span>: error.message, <span class="hljs-attr">type</span>: <span class="hljs-string">'comments'</span> }))
]);
<span class="hljs-comment">// Process results and handle partial failures</span>
<span class="hljs-keyword">const</span> [userResult, postsResult, commentsResult] = results;
<span class="hljs-keyword">return</span> {
<span class="hljs-attr">user</span>: userResult.status === <span class="hljs-string">'fulfilled'</span> ? userResult.value : <span class="hljs-literal">null</span>,
<span class="hljs-attr">posts</span>: postsResult.status === <span class="hljs-string">'fulfilled'</span> ? postsResult.value : [],
<span class="hljs-attr">comments</span>: commentsResult.status === <span class="hljs-string">'fulfilled'</span> ? commentsResult.value : [],
<span class="hljs-attr">errors</span>: results
.filter(<span class="hljs-function"><span class="hljs-params">result</span> =></span> result.status === <span class="hljs-string">'rejected'</span> || result.value?.error)
.map(<span class="hljs-function"><span class="hljs-params">result</span> =></span> result.reason || result.value?.error)
};
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to fetch data:'</span>, error);
<span class="hljs-keyword">throw</span> error;
}
}
Why this is better:
-
Operations run in parallel, reducing total execution time
-
Network and CPU resources are used more efficiently
-
The application feels more responsive to users
-
Can handle partial failures gracefully
- Don’t Mix Patterns Unnecessarily
Mixing Promise chains with async/await in the same function creates inconsistent code that’s harder to read, debug, and maintain. It can also lead to subtle bugs and makes the code flow less predictable.
Avoid this:
javascript<span class="hljs-comment">// Mixing async/await with .then() - this is confusing!</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mixedPattern</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> fetchData();
<span class="hljs-comment">// Suddenly switching to Promise chain style</span>
<span class="hljs-keyword">return</span> processData(data).then(<span class="hljs-function"><span class="hljs-params">result</span> =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Processing complete'</span>);
<span class="hljs-keyword">return</span> saveResult(result);
}).then(<span class="hljs-function"><span class="hljs-params">savedResult</span> =></span> {
<span class="hljs-keyword">return</span> savedResult.id;
}).catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error in promise chain:'</span>, error);
<span class="hljs-keyword">throw</span> error;
});
}
Why this is problematic:
-
Inconsistent error handling patterns (try/catch vs .catch())
-
Harder to follow the execution flow
-
Different debugging experiences
-
More opportunities for bugs
-
Team members need to understand multiple patterns
This often happens when developers are working with existing Promise-based code and try to gradually introduce async/await without fully converting the function.
Do this instead:
javascript<span class="hljs-comment">// Consistent async/await - much clearer!</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">consistentPattern</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> fetchData();
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Data fetched'</span>);
<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> processData(data);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Processing complete'</span>);
<span class="hljs-keyword">const</span> savedResult = <span class="hljs-keyword">await</span> saveResult(result);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Result saved'</span>);
<span class="hljs-keyword">return</span> savedResult.id;
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error in async function:'</span>, error);
<span class="hljs-keyword">throw</span> error;
}
}
Alternative consistent Promise approach:
javascript<span class="hljs-comment">// If you prefer Promises, be consistent with that too</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">consistentPromisePattern</span>(<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">return</span> fetchData()
.then(<span class="hljs-function"><span class="hljs-params">data</span> =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Data fetched'</span>);
<span class="hljs-keyword">return</span> processData(data);
})
.then(<span class="hljs-function"><span class="hljs-params">result</span> =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Processing complete'</span>);
<span class="hljs-keyword">return</span> saveResult(result);
})
.then(<span class="hljs-function"><span class="hljs-params">savedResult</span> =></span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Result saved'</span>);
<span class="hljs-keyword">return</span> savedResult.id;
})
.catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error in promise chain:'</span>, error);
<span class="hljs-keyword">throw</span> error;
});
}
Use Descriptive Variable Names in Async Functions
javascript<span class="hljs-comment">// Poor naming</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">process</span>(<span class="hljs-params">id</span>) </span>{
<span class="hljs-keyword">const</span> d = <span class="hljs-keyword">await</span> fetch(id);
<span class="hljs-keyword">const</span> r = <span class="hljs-keyword">await</span> transform(d);
<span class="hljs-keyword">return</span> r;
}
<span class="hljs-comment">// Better naming</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processUserProfile</span>(<span class="hljs-params">userId</span>) </span>{
<span class="hljs-keyword">const</span> userData = <span class="hljs-keyword">await</span> fetchUserData(userId);
<span class="hljs-keyword">const</span> transformedProfile = <span class="hljs-keyword">await</span> transformUserData(userData);
<span class="hljs-keyword">return</span> transformedProfile;
}
Handle Timeouts for Long-Running Operations
javascript<span class="hljs-comment">// Add timeout wrapper for reliability</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withTimeout</span>(<span class="hljs-params">promise, timeoutMs</span>) </span>{
<span class="hljs-keyword">const</span> timeout = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">_, reject</span>) =></span> {
<span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =></span> reject(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Operation timed out'</span>)), timeoutMs);
});
<span class="hljs-keyword">return</span> <span class="hljs-built_in">Promise</span>.race([promise, timeout]);
}
<span class="hljs-comment">// Usage</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">reliableDataFetch</span>(<span class="hljs-params">userId</span>) </span>{
<span class="hljs-keyword">try</span> {
<span class="hljs-comment">// Timeout after 5 seconds</span>
<span class="hljs-keyword">const</span> userData = <span class="hljs-keyword">await</span> withTimeout(fetchUserData(userId), <span class="hljs-number">5000</span>);
<span class="hljs-keyword">return</span> userData;
} <span class="hljs-keyword">catch</span> (error) {
<span class="hljs-keyword">if</span> (error.message === <span class="hljs-string">'Operation timed out'</span>) {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Request timed out, using cached data'</span>);
<span class="hljs-keyword">return</span> getCachedUserData(userId);
}
<span class="hljs-keyword">throw</span> error;
}
}
Making the Right Choice
Here’s a decision framework to help you choose:
Choose Promises When:
-
Working with libraries that return Promises
-
Writing functional programming style code
-
Creating utility functions that other code will chain
-
You need fine-grained control over Promise behavior
-
Working with existing Promise-based codebases
Choose Async/Await When:
-
Writing new application code
-
You need sequential operations with a clear flow
-
Complex error handling is required
-
Working with conditional asynchronous logic
-
Code readability is a priority
-
You’re building modern JavaScript applications
Consider Both When:
-
Using
Promise.all()
,Promise.race()
, orPromise.allSettled()
-
Building complex asynchronous flows
-
You need both the power of Promises and the readability of Async/Await
Conclusion
Both Promises and Async/Await are powerful tools for handling asynchronous operations in JavaScript. Promises provide flexibility and fine-grained control, while Async/Await offers cleaner, more readable code that’s easier to debug and maintain.
In modern JavaScript development, Async/Await is generally preferred for application code due to its readability and ease of use. However, understanding Promises is still crucial since Async/Await is built on top of them, and many libraries and APIs still use Promises directly.
The key is to understand both approaches and choose the one that best fits your specific use case, team preferences, and project requirements. Remember that you can always mix both approaches when it makes sense – use Async/Await for your main application logic and Promises for utility functions and library integrations.
By mastering both techniques very well, you will be well-equipped to handle any asynchronous programming challenge in JavaScript.
Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & MoreÂ