My async code used to feel like a tangle of errors I couldn’t unravel quickly. Each failed promise would spit out its own error, leaving me to stitch together a solution.
Then I discovered AggregateError
, and it changed how I debug. It’s a JavaScript feature that bundles multiple errors into one object, perfect for complex async tasks.
Imagine validating a form where multiple fields are checked asynchronously. Without AggregateError
, you’re stuck catching each error separately, which bloats your code and confuses users. It’s a slog to debug and deliver clear feedback.
The Old, Clunky Way
Without AggregateError
, you’d handle each validation error individually. It’s tedious and error-prone.
const fetchFromApi1 = () => Promise.reject(new Error("API 1 failed"));
const fetchFromApi2 = () => Promise.reject(new Error("API 2 failed"));
const fetchFromApi3 = () => Promise.reject(new Error("API 3 failed"));
async function fetchWithoutAggregateError(apis, retries = 3, delay = 1000) {
try {
const data = await Promise.any(apis);
console.log("Data fetched:", data);
return data;
} catch (e) {
console.log("All promises failed.");
// Manually handle each promise rejection by checking which promises failed
for (let i = 0; i < apis.length; i++) {
try {
await apis[i];
} catch (error) {
console.log(`Error from API ${i + 1}:`, error.message);
}
}
if (retries > 0) {
console.log(`Retrying... (${retries} attempts left)`);
await new Promise((resolve) => setTimeout(resolve, delay));
return fetchWithoutAggregateError(apis, retries - 1, delay);
} else {
console.log(
"All data sources failed after multiple attempts. Please try again later."
);
throw new Error("All data sources failed after retries.");
}
}
}
// Start fetching data with retries
fetchWithoutAggregateError([fetchFromApi1(), fetchFromApi2(), fetchFromApi3()]);
This approach requires a try/catch for each check. It’s repetitive and hard to maintain.
function validateUserWithoutAggregateError(user) {
if (!user.name) {
throw new Error("Name is required");
}
if (!user.email) {
throw new Error("Email is required");
}
if (user.age < 18) {
throw new Error("User must be at least 18 years old");
}
return true;
}
try {
validateUserWithoutAggregateError({ name: "", email: "", age: 17 });
} catch (e) {
console.log(e.message); // Only the first error is caught and displayed
// Output: "Name is required"
}
🔴 Multiple try/catch blocks clutter your code.
🔴 Users get fragmented error messages, hurting their experience.
Here’s how to do it
When Promise.any()
looks for the first resolved promise and all promises reject, JavaScript raises an AggregateError
. This error collects all rejection reasons into a single object.
const fetchFromApi1 = () => Promise.reject(new Error("API 1 failed"));
const fetchFromApi2 = () => Promise.reject(new Error("API 2 failed"));
const fetchFromApi3 = () => Promise.reject(new Error("API 3 failed"));
async function fetchWithRetry(apis, retries = 3, delay = 1000) {
try {
const data = await Promise.any(apis);
console.log("Data fetched:", data);
return data;
} catch (e) {
if (e instanceof AggregateError) {
console.log(e.name); // "AggregateError"
console.log(e.message); // "All promises were rejected"
console.log("Errors:", e.errors); // [Error: API 1 failed, Error: API 2 failed, Error: API 3 failed]
if (retries > 0) {
console.log(`Retrying... (${retries} attempts left)`);
await new Promise((resolve) => setTimeout(resolve, delay));
return fetchWithRetry(apis, retries - 1, delay);
} else {
console.log(
"All data sources failed after multiple attempts. Please try again later."
);
throw e; // Re-throw the error after exhausting retries
}
} else {
// If the error is not an AggregateError, rethrow it
throw e;
}
}
}
// Start fetching data with retries
fetchWithRetry([fetchFromApi1(), fetchFromApi2(), fetchFromApi3()]);
- When using
Promise.any()
to fetch data from multiple APIs, it resolves with the first successful response. If all promises reject, it raises anAggregateError
. - Wrap the operation in a try/catch block: the try block processes successful data, while the catch block captures the
AggregateError
when all promises fail. - If an
AggregateError
occurs, I log its details and retry the operation up to a set number of attempts, adding a delay between each try. - Retry logic works by recursively calling the function, decrementing the retry count each time, until a success occurs or all retries are exhausted.
- If retries run out, I log a final error message and rethrow the AggregateError for further handling if needed.
AggregateError
streamlines managing multiple async failures, making complex error handling cleaner. It’s a tool you’ll appreciate when debugging chaotic async flows.
function validateUser(user) {
let errors = [];
if (!user.name) {
errors.push(new Error("Name is required"));
}
if (!user.email) {
errors.push(new Error("Email is required"));
}
if (user.age < 18) {
errors.push(new Error("User must be at least 18 years old"));
}
if (errors.length > 0) {
throw new AggregateError(errors, "Validation failed");
}
return true;
}
try {
validateUser({ name: "", email: "", age: 17 });
} catch (e) {
if (e instanceof AggregateError) {
console.log(e.name); // "AggregateError"
console.log(e.message); // "Validation failed"
e.errors.forEach((err) => console.log(err.message));
// Output:
// "Name is required"
// "Email is required"
// "User must be at least 18 years old"
}
}
This code runs all checks and collects errors in one shot. You can show users a clear list of issues without extra logic.
✅ Consolidates errors for cleaner code.
✅ Makes form validation logic easier to manage.
✅ Delivers clear feedback to users, fast.
Final Takeaway
AggregateError
turns chaotic error handling into a smooth process. It’s a game-saver for form validation or any async task with multiple points of failure. Give it a spin in your next form-heavy app.
Follow me for more error-handling tricks!
Top comments (0)