How to Write Clean, Scalable Async/Await Code in JavaScript (Without Unhandled Rejections)


While async/await makes asynchronous code look synchronous, treating it identically to standard code leads to performance bottlenecks and hidden bugs. Common mistakes include running requests sequentially when they could run in parallel, or wrapping everything in messy try/catch blocks that clutter your business logic.
Follow these four steps to clean up your async code and make it rock-solid:


Step 1: Parallelize Independent Operations
A common antipattern is awaiting independent API calls back-to-back, which doubles or triples your total execution time.
Bad Pattern (Sequential):


JavaScript
const user = await fetchUser(userId); // Takes 200ms
const posts = await fetchPosts(userId); // Takes 300ms (Total: 500ms)
Action (Parallel): Use Promise.all or Promise.allSettled to execute independent promises concurrently.


JavaScript
const [user, posts] = await Promise.all([
fetchUser(userId),
fetchPosts(userId)
]); // Total: ~300ms


Step 2: Use Promise.allSettled for Non-Critical Operations
If you are fetching multiple resources and don't want a single minor failure (e.g., fetching user notifications) to break the entire page load, avoid Promise.all because it short-circuits on the first error.
Action: Use Promise.allSettled to handle partial successes gracefully.


JavaScript
const results = await Promise.allSettled([fetchProfile(), fetchAds()]);
const profile = results[0].status === 'fulfilled' ? results[0].value : null;
Step 3: Implement a Clean Error-Handling Wrapper
Instead of littering your controllers with repetitive try/catch blocks, use a lightweight utility wrapper function (similar to the Go programming language style).
Action: Create a reusable helper to handle promise errors cleanly without throwing unhandled exceptions:


JavaScript
const to = (promise) =>
promise
.then((data) => [null, data])
.catch((err) => [err, null]);
// Usage:
const [err, user] = await to(fetchUser(userId));
if (err) return handleUserError(err);


Step 4: Always Cancel Unmounted/Aborted Requests
In frontend frameworks (or Node.js microservices), unhandled async responses after a component unmounts or a client disconnects can lead to memory leaks or state corruption.
Action: Pass an AbortController signal to your fetch or HTTP client requests to cancel pending operations cleanly when they are no longer needed.


Key Takeaways
Concurrency over Sequentiality: Use Promise.all whenever operations are independent of one another.
Fail Gracefully: Choose Promise.allSettled when partial success is acceptable.
Clean Control Flow: Use wrapper utilities or dedicated middleware to keep async error handling readable and consistent.


CTA
Looking to sharpen your coding skills and master software architecture? Join thousands of software developers, frontend engineers, and backend specialists building cleaner, faster applications. Join the Developers & Coding Community today!
How to Write Clean, Scalable Async/Await Code in JavaScript (Without Unhandled Rejections) While async/await makes asynchronous code look synchronous, treating it identically to standard code leads to performance bottlenecks and hidden bugs. Common mistakes include running requests sequentially when they could run in parallel, or wrapping everything in messy try/catch blocks that clutter your business logic. Follow these four steps to clean up your async code and make it rock-solid: Step 1: Parallelize Independent Operations A common antipattern is awaiting independent API calls back-to-back, which doubles or triples your total execution time. Bad Pattern (Sequential): JavaScript const user = await fetchUser(userId); // Takes 200ms const posts = await fetchPosts(userId); // Takes 300ms (Total: 500ms) Action (Parallel): Use Promise.all or Promise.allSettled to execute independent promises concurrently. JavaScript const [user, posts] = await Promise.all([ fetchUser(userId), fetchPosts(userId) ]); // Total: ~300ms Step 2: Use Promise.allSettled for Non-Critical Operations If you are fetching multiple resources and don't want a single minor failure (e.g., fetching user notifications) to break the entire page load, avoid Promise.all because it short-circuits on the first error. Action: Use Promise.allSettled to handle partial successes gracefully. JavaScript const results = await Promise.allSettled([fetchProfile(), fetchAds()]); const profile = results[0].status === 'fulfilled' ? results[0].value : null; Step 3: Implement a Clean Error-Handling Wrapper Instead of littering your controllers with repetitive try/catch blocks, use a lightweight utility wrapper function (similar to the Go programming language style). Action: Create a reusable helper to handle promise errors cleanly without throwing unhandled exceptions: JavaScript const to = (promise) => promise .then((data) => [null, data]) .catch((err) => [err, null]); // Usage: const [err, user] = await to(fetchUser(userId)); if (err) return handleUserError(err); Step 4: Always Cancel Unmounted/Aborted Requests In frontend frameworks (or Node.js microservices), unhandled async responses after a component unmounts or a client disconnects can lead to memory leaks or state corruption. Action: Pass an AbortController signal to your fetch or HTTP client requests to cancel pending operations cleanly when they are no longer needed. Key Takeaways Concurrency over Sequentiality: Use Promise.all whenever operations are independent of one another. Fail Gracefully: Choose Promise.allSettled when partial success is acceptable. Clean Control Flow: Use wrapper utilities or dedicated middleware to keep async error handling readable and consistent. CTA Looking to sharpen your coding skills and master software architecture? Join thousands of software developers, frontend engineers, and backend specialists building cleaner, faster applications. Join the Developers & Coding Community today!
0 Yorumlar 0 hisse senetleri 225 Views 0 önizleme