Troubleshooting Tuesday: Debugging Async/Await in JavaScript - The 5 Most Common Errors

On Tuesdays, we focus on solving real problems. Today, we analyze the most frustrating errors when working with async/await in JavaScript and systematic techniques to identify and resolve them before they cause problems in production.
Problem #1: Forgetting Await - The Silent Promise Killer
Symptoms:
- Code that continues executing without waiting for results
- Variables with unexpected
undefinedvalues - Unresolved promises left hanging
- Inconsistent behavior in different executions
Problematic Code:
// β Forgetting await causes incorrect execution
async function processUserData(userId) {
const user = getUserById(userId); // Missing await!
console.log(user.name); // Error: Cannot read property 'name' of undefined
const orders = getOrdersByUser(user.id); // Missing await!
return orders.length; // Returns Promise instead of the number
}
// β Await in non-async function
function saveData(data) {
const result = await saveToDatabase(data); // SyntaxError!
return result;
}
Systematic Debugging:
// β
Always verify that async functions have await
async function processUserData(userId) {
// Explicit await
const user = await getUserById(userId);
console.log(user.name); // Now works correctly
const orders = await getOrdersByUser(user.id);
return orders.length; // Returns the correct number
}
// β
Mark function as async if using await
async function saveData(data) {
const result = await saveToDatabase(data);
return result;
}
Detection Tool:
// ESLint rule to detect floating promises
// .eslintrc.js
module.exports = {
rules: {
'no-floating-promises': 'error',
'@typescript-eslint/no-floating-promises': 'error'
}
};
// Custom hook for promise debugging
function createPromiseTracker() {
const tracked = new Set();
const originalThen = Promise.prototype.then;
Promise.prototype.then = function(...args) {
tracked.add(this);
const result = originalThen.apply(this, args);
result.finally(() => {
tracked.delete(this);
});
return result;
};
// Check for pending promises
setInterval(() => {
if (tracked.size > 0) {
console.warn(`β οΈ ${tracked.size} unresolved promises detected`);
}
}, 5000);
}
// Activate in development
if (process.env.NODE_ENV === 'development') {
createPromiseTracker();
}
Problem #2: Incomplete Error Handling in Async/Await
Symptoms:
- Errors that crash the application without being caught
- Try-catch that doesnβt cover all cases
- Unhandled rejected promises (UnhandledPromiseRejection)
- Confusing error stack traces
Problematic Code:
// β Try-catch only in part of the code
async function fetchMultipleResources() {
try {
const user = await getUser();
const posts = await getPosts(user.id);
// This code is outside the effective try-catch
const comments = await getComments(posts[0].id); // If it fails, it's not caught
return { user, posts, comments };
} catch (error) {
console.error('Error:', error);
}
// β Post-fetch processing without protection
await processData(data); // Error not caught
}
// β Generic catch without context
async function saveRecord(record) {
try {
return await database.save(record);
} catch (error) {
console.log('Error saving'); // Doesn't show what failed or why
throw error; // Re-throws without additional context
}
}
Robust Error Handling:
// β
Complete try-catch with context
async function fetchMultipleResources(userId) {
try {
const user = await getUser(userId);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
await processData({ user, posts, comments });
return { user, posts, comments };
} catch (error) {
// Add context to the error
const enhancedError = new Error(
`Failed to fetch resources for user ${userId}: ${error.message}`
);
enhancedError.originalError = error;
enhancedError.userId = userId;
enhancedError.timestamp = new Date().toISOString();
console.error('Resource fetch error:', {
message: enhancedError.message,
stack: error.stack,
userId
});
throw enhancedError;
}
}
// β
Error handling with specific types
async function saveRecord(record) {
try {
return await database.save(record);
} catch (error) {
// Handle different types of errors
if (error.code === 'ECONNREFUSED') {
throw new Error('Database connection failed. Check if database is running.');
} else if (error.code === '23505') {
throw new Error(`Record with ID ${record.id} already exists.`);
} else if (error.name === 'ValidationError') {
throw new Error(`Invalid record data: ${error.message}`);
} else {
throw new Error(`Database error: ${error.message}`);
}
}
}
```### π Centralized Error Handler:
```javascript
// Structured error handling system
class AsyncErrorHandler {
constructor() {
this.errorListeners = [];
this.setupGlobalHandlers();
}
setupGlobalHandlers() {
// Capture unhandled rejected promises
if (typeof window !== 'undefined') {
window.addEventListener('unhandledrejection', (event) => {
console.error('π¨ Unhandled Promise Rejection:', {
reason: event.reason,
promise: event.promise,
stack: event.reason?.stack
});
this.notifyListeners({
type: 'UNHANDLED_REJECTION',
error: event.reason,
timestamp: Date.now()
});
// Prevent the browser from handling the error by default
event.preventDefault();
});
}
// Node.js
if (typeof process !== 'undefined') {
process.on('unhandledRejection', (reason, promise) => {
console.error('π¨ Unhandled Rejection at:', promise, 'reason:', reason);
this.notifyListeners({
type: 'UNHANDLED_REJECTION',
error: reason,
timestamp: Date.now()
});
});
}
}
// Wrapper for async functions with error handling
wrap(asyncFn, context = '') {
return async (...args) => {
try {
return await asyncFn(...args);
} catch (error) {
const enhancedError = {
message: error.message,
stack: error.stack,
context,
args: JSON.stringify(args),
timestamp: new Date().toISOString()
};
console.error(`Error in ${context}:`, enhancedError);
this.notifyListeners(enhancedError);
throw error;
}
};
}
addListener(callback) {
this.errorListeners.push(callback);
}
notifyListeners(errorInfo) {
this.errorListeners.forEach(listener => {
try {
listener(errorInfo);
} catch (err) {
console.error('Error in error listener:', err);
}
});
}
}
// Usage
const errorHandler = new AsyncErrorHandler();
// Wrap critical functions
const safeFetchUser = errorHandler.wrap(fetchUser, 'fetchUser');
const safeSaveData = errorHandler.wrap(saveData, 'saveData');
// Monitor errors
errorHandler.addListener((error) => {
// Send to error tracking service
sendToErrorTracking(error);
});
Problem #3: Async/Await in Loops - Sequential vs Parallel
Symptoms:
- Operations taking too long
- Slow processing of large arrays
- API requests running one by one unnecessarily
- Degraded performance without apparent reason
Problematic Code:
// β Unnecessary sequential execution
async function processUsers(userIds) {
const results = [];
// Each iteration waits for the previous one (very slow!)
for (const id of userIds) {
const user = await fetchUser(id); // 1 second each
const data = await processUserData(user); // 2 seconds each
results.push(data);
}
return results; // 100 users = 300 seconds!
}
// β forEach with async doesn't work as expected
async function updateAllUsers(users) {
users.forEach(async (user) => {
await updateUser(user); // These awaits are not waited for!
});
console.log('Done!'); // Executes immediately, doesn't wait
}
Solution with Parallel Execution:
// β
Parallel execution with Promise.all
async function processUsers(userIds) {
// All operations start at the same time
const userPromises = userIds.map(id => fetchUser(id));
const users = await Promise.all(userPromises);
const dataPromises = users.map(user => processUserData(user));
const results = await Promise.all(dataPromises);
return results; // 100 users = ~3 seconds (the slowest one)
}
// β
Parallel execution with concurrency limit
async function processUsersWithLimit(userIds, limit = 5) {
const results = [];
// Process in chunks to avoid overload
for (let i = 0; i < userIds.length; i += limit) {
const chunk = userIds.slice(i, i + limit);
const chunkResults = await Promise.all(
chunk.map(async (id) => {
const user = await fetchUser(id);
return processUserData(user);
})
);
results.push(...chunkResults);
}
return results;
}
// β
for...of when you need sequential execution
async function updateAllUsers(users) {
for (const user of users) {
await updateUser(user); // Waits for each one before continuing
}
console.log('Done!'); // Now it waits for all
}
```### π― Utility Functions for Different Scenarios:
```javascript
// Promise.all - All must succeed
async function fetchAllOrFail(urls) {
try {
const responses = await Promise.all(
urls.map(url => fetch(url))
);
return responses;
} catch (error) {
console.error('At least one request failed:', error);
throw error;
}
}
// Promise.allSettled - Execute all, even if some fail
async function fetchAllWithResults(urls) {
const results = await Promise.allSettled(
urls.map(url => fetch(url))
);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
console.log(`Successful: ${successful.length}, Failed: ${failed.length}`);
return { successful, failed };
}
// Promise.race - The first to complete
async function fetchWithTimeout(url, timeoutMs = 5000) {
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Request timeout')), timeoutMs);
});
return Promise.race([
fetch(url),
timeoutPromise
]);
}
// Batch processing with concurrency control
class BatchProcessor {
constructor(concurrency = 3) {
this.concurrency = concurrency;
this.queue = [];
this.running = 0;
}
async process(items, handler) {
const results = [];
return new Promise((resolve, reject) => {
let index = 0;
const processNext = async () => {
if (index >= items.length && this.running === 0) {
resolve(results);
return;
}
while (this.running < this.concurrency && index < items.length) {
const currentIndex = index++;
const item = items[currentIndex];
this.running++;
handler(item)
.then(result => {
results[currentIndex] = result;
})
.catch(error => {
console.error(`Error processing item ${currentIndex}:`, error);
results[currentIndex] = { error: error.message };
})
.finally(() => {
this.running--;
processNext();
});
}
};
processNext();
});
}
}
// Usage
const processor = new BatchProcessor(5); // 5 concurrent operations
const results = await processor.process(userIds, async (id) => {
const user = await fetchUser(id);
return processUserData(user);
});
Problem #4: Race Conditions in Async Code
Symptoms:
- Inconsistent results in different executions
- Data that overwrites each other
- UI state desynchronized
- Obsolete requests processed after new ones
Problematic Code:
// β Race condition in search
async function handleSearch(query) {
setLoading(true);
const results = await searchAPI(query);
// If the user typed something else while we were waiting,
// these results are no longer relevant
setResults(results);
setLoading(false);
}
// β Multiple concurrent updates
async function incrementCounter() {
const current = await getCounter();
const newValue = current + 1;
await saveCounter(newValue); // Race condition if called multiple times
}
Solution with Request Cancellation:
// β
Cancel obsolete requests
class SearchManager {
constructor() {
this.currentController = null;
this.requestId = 0;
}
async search(query) {
// Cancel previous request
if (this.currentController) {
this.currentController.abort();
}
// Create new controller
this.currentController = new AbortController();
const currentRequestId = ++this.requestId;
try {
setLoading(true);
const results = await fetch(`/api/search?q=${query}`, {
signal: this.currentController.signal
});
// Verify that this is still the most recent request
if (currentRequestId === this.requestId) {
const data = await results.json();
setResults(data);
} else {
console.log('Discarding obsolete results');
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request canceled');
} else {
console.error('Search error:', error);
}
} finally {
if (currentRequestId === this.requestId) {
setLoading(false);
}
}
}
}
const searchManager = new SearchManager();
// Usage in input handler
function handleSearchInput(event) {
searchManager.search(event.target.value);
}
Solution with Locks/Mutexes:
// β
Implement mutex for critical operations
class AsyncMutex {
constructor() {
this.queue = [];
this.locked = false;
}
async acquire() {
if (!this.locked) {
this.locked = true;
return () => this.release();
}
return new Promise(resolve => {
this.queue.push(() => {
resolve(() => this.release());
});
});
}
release() {
if (this.queue.length > 0) {
const next = this.queue.shift();
next();
} else {
this.locked = false;
}
}
async runExclusive(callback) {
const release = await this.acquire();
try {
return await callback();
} finally {
release();
}
}
}
// Usage to prevent race conditions
const counterMutex = new AsyncMutex();
async function incrementCounter() {
await counterMutex.runExclusive(async () => {
const current = await getCounter();
const newValue = current + 1;
await saveCounter(newValue);
});
}
// Multiple calls are now safe
await Promise.all([
incrementCounter(),
incrementCounter(),
incrementCounter()
]); // Counter will be 3, not undefined
Problem #5: Debugging Async Stack Traces
Symptoms:
- Stack traces that do not show where the error originated
- Difficulty tracking the execution flow
- Errors that seem to come from βnowhereβ
- Console.log that does not show the expected order
Problematic Code:
// β Lost stack trace
async function processData() {
const data = await fetchData();
return transformData(data); // Error here shows limited stack
}
function transformData(data) {
return data.items.map(item => {
return item.value.toUpperCase(); // Error if value is null
});
}
// When it fails, the stack trace does not show the full context
```### β
Enhanced Debugging:
```javascript
// β
Async stack traces with context
async function processDataWithContext() {
console.log('π΅ Starting processData');
try {
console.log(' π₯ Fetching data...');
const data = await fetchData();
console.log(' β
Data fetched:', { itemCount: data.items.length });
console.log(' π Transforming data...');
const result = transformData(data);
console.log(' β
Transform complete');
return result;
} catch (error) {
// Add context to the stack trace
console.error('β Error in processData:', {
message: error.message,
stack: error.stack,
phase: 'processing',
timestamp: new Date().toISOString()
});
throw error;
}
}
function transformData(data) {
return data.items.map((item, index) => {
try {
return item.value.toUpperCase();
} catch (error) {
throw new Error(
`Transform failed at index ${index}: ${error.message}`
);
}
});
}
Async Debugging Tools:
// Utility for tracing async operations
class AsyncTracer {
constructor() {
this.traces = new Map();
this.traceId = 0;
}
start(operationName) {
const id = ++this.traceId;
this.traces.set(id, {
id,
name: operationName,
startTime: Date.now(),
events: [],
status: 'running'
});
console.log(`π’ [${id}] Started: ${operationName}`);
return {
id,
log: (message, data) => this.log(id, message, data),
error: (error) => this.error(id, error),
complete: () => this.complete(id)
};
}
log(id, message, data = {}) {
const trace = this.traces.get(id);
if (!trace) return;
const event = {
timestamp: Date.now(),
duration: Date.now() - trace.startTime,
message,
data
};
trace.events.push(event);
console.log(` π [${id}] ${message}`, data);
}
error(id, error) {
const trace = this.traces.get(id);
if (!trace) return;
trace.status = 'error';
trace.error = {
message: error.message,
stack: error.stack,
timestamp: Date.now()
};
console.error(`β [${id}] Error:`, {
operation: trace.name,
duration: Date.now() - trace.startTime,
error: error.message,
events: trace.events
});
}
complete(id) {
const trace = this.traces.get(id);
if (!trace) return;
trace.status = 'completed';
trace.endTime = Date.now();
trace.duration = trace.endTime - trace.startTime;
console.log(`β
[${id}] Completed: ${trace.name} (${trace.duration}ms)`);
return trace;
}
getTrace(id) {
return this.traces.get(id);
}
getAllTraces() {
return Array.from(this.traces.values());
}
}
// Usage
const tracer = new AsyncTracer();
async function complexOperation() {
const trace = tracer.start('complexOperation');
try {
trace.log('Fetching user data');
const user = await fetchUser();
trace.log('Processing user', { userId: user.id });
const processed = await processUser(user);
trace.log('Saving results');
await saveResults(processed);
trace.complete();
return processed;
} catch (error) {
trace.error(error);
throw error;
}
}
Performance Tracking:
// Decorator to measure performance of async functions
function measurePerformance(target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = async function(...args) {
const start = performance.now();
const label = `${target.constructor.name}.${propertyKey}`;
console.time(label);
try {
const result = await originalMethod.apply(this, args);
const duration = performance.now() - start;
console.timeEnd(label);
console.log(`β±οΈ ${label} completed in ${duration.toFixed(2)}ms`);
return result;
} catch (error) {
const duration = performance.now() - start;
console.timeEnd(label);
console.error(`β ${label} failed after ${duration.toFixed(2)}ms:`, error);
throw error;
}
};
return descriptor;
}
// Usage with classes
class DataService {
@measurePerformance
async fetchData() {
// Implementation
}
@measurePerformance
async processData(data) {
// Implementation
}
}
Async Debugging Tools
Chrome DevTools Async Stack Traces:
// Enable in DevTools: Settings β Experiments β Enable async stack traces
// Use debugger statements strategically
async function debugAsyncFlow() {
debugger; // Pause here
const data = await fetchData();
debugger; // Pause after the fetch
const processed = await processData(data);
debugger; // Pause after processing
return processed;
}
Node.js Async Hooks:
// For Node.js applications
const async_hooks = require('async_hooks');
const asyncOperations = new Map();
const hook = async_hooks.createHook({
init(asyncId, type, triggerAsyncId) {
asyncOperations.set(asyncId, {
type,
triggerAsyncId,
timestamp: Date.now()
});
},
destroy(asyncId) {
const operation = asyncOperations.get(asyncId);
if (operation) {
const duration = Date.now() - operation.timestamp;
if (duration > 1000) {
console.warn(`β οΈ Long-running async operation: ${operation.type} (${duration}ms)`);
}
}
asyncOperations.delete(asyncId);
}
});
// Enable in development
if (process.env.NODE_ENV === 'development') {
hook.enable();
}
Async/Await Debugging Checklist
Code Verification:
- All async functions have
asynckeyword - All promises have
await(or are handled with.then()) - Try-catch covers all relevant async code
- Async loops are optimized (parallel vs sequential)
- No race conditions in concurrent operations
Error Handling:
- Specific errors with descriptive context
- Handler for
unhandledRejectionimplemented - Stack traces preserve useful information
- Errors are logged with sufficient detail
Performance:
- Independent operations run in parallel
- Concurrency limits implemented where needed
- Timeouts configured to prevent hangs
- No unnecessary awaits in loops
Testing:
- Tests cover success and error cases
- Tests verify behavior with delays
- Race conditions tested with concurrent requests
- Memory leaks verified in long-running operations
Pro Tips for Async/Await
1. Use ESLint with specific rules:
// .eslintrc.js
module.exports = {
extends: ['plugin:promise/recommended'],
rules: {
'no-async-promise-executor': 'error',
'require-atomic-updates': 'error',
'no-await-in-loop': 'warn',
'promise/catch-or-return': 'error',
'promise/no-nesting': 'warn'
}
};
2. Explicit return type (TypeScript):
// β
Explicit types prevent errors
async function fetchUser(id: string): Promise<User> {
co```typescript
const response = await fetch(`/api/users/${id}`);
return response.json(); // TypeScript checks that it returns User
}
3. Utility function for retry logic:
async function retryAsync(fn, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
if (i === retries - 1) throw error;
console.log(`Retry ${i + 1}/${retries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
}
}
}
// Usage
const data = await retryAsync(() => fetchData(), 3, 1000);
Statistics of Interest
According to production error analysis:
- 45% of async/await bugs are due to forgetting
await - 28% are unhandled race conditions
- 18% are errors not properly caught
- 9% are performance issues due to unnecessary sequential execution
The average debugging time for async errors:
- Unresolved Promises: 30-60 minutes
- Race conditions: 1-3 hours
- Complex stack traces: 2-4 hours
Open Discussion
Which of these async/await errors has given you the most headaches?
Do you have any debugging techniques for async code that I didnβt mention?
How do you handle testing async code in your projects?
What tools do you use to track asynchronous operations in production?
Asynchronous code can be one of the most challenging aspects of JavaScript, but with the right tools and techniques, debugging becomes much more manageable. The key is to anticipate common problems and establish robust patterns from the start.
Letβs share experiences and techniques for writing more reliable and debuggable async code.
#TroubleshootingTuesday javascript asyncawait #Promises debugging webdev nodejs #ErrorHandling performance