# How to debug async code in Replit

- Tool: Replit
- Difficulty: Beginner
- Time required: 15 minutes
- Compatibility: All Replit plans (Starter, Core, Pro, Enterprise)
- Last updated: March 2026

## TL;DR

Debug async code in Replit by adding structured logging at every await point, wrapping async functions in try-catch blocks, and handling unhandled Promise rejections globally. Use console.log with timestamps and labels to trace the order of async operations, since asynchronous code does not execute in the order it appears. The Console pane shows server-side output while the Preview DevTools shows client-side logs. For Python, use asyncio debug mode and logging instead of print statements.

## Debug Async JavaScript and Python Code in Replit

Asynchronous code is notoriously difficult to debug because operations complete in unpredictable order, errors can be silently swallowed by unhandled Promises, and stack traces often point to the wrong location. This tutorial shows you practical debugging techniques that work in Replit's browser-based environment: structured logging to trace execution order, proper error handling with try-catch and .catch(), global handlers for unhandled rejections, and using Replit's Console and Preview DevTools to inspect output.

## Before you start

- A Replit account (any plan)
- Basic understanding of async/await and Promises in JavaScript or asyncio in Python
- A project with async code you want to debug
- Familiarity with the Replit Console and Shell tools

## Step-by-step guide

### 1. Add structured logging at every await point

The most effective async debugging technique is adding log statements before and after every await call. Label each log with the function name and step number so you can trace the exact execution order. Include timestamps to measure how long each async operation takes. In Replit, server-side logs appear in the Console pane when you click Run. This approach works better than traditional breakpoints for async code because it lets you see the interleaved execution order of multiple concurrent operations.

```
// Structured logging for async debugging
async function fetchUserData(userId) {
  console.log(`[fetchUserData] START — userId: ${userId}, time: ${Date.now()}`);

  try {
    console.log(`[fetchUserData] Step 1: Fetching profile...`);
    const profile = await fetch(`/api/users/${userId}`);
    console.log(`[fetchUserData] Step 1 DONE — status: ${profile.status}`);

    console.log(`[fetchUserData] Step 2: Parsing JSON...`);
    const data = await profile.json();
    console.log(`[fetchUserData] Step 2 DONE — keys: ${Object.keys(data)}`);

    console.log(`[fetchUserData] Step 3: Fetching orders...`);
    const orders = await fetch(`/api/users/${userId}/orders`);
    console.log(`[fetchUserData] Step 3 DONE — status: ${orders.status}`);

    return data;
  } catch (error) {
    console.error(`[fetchUserData] ERROR: ${error.message}`);
    throw error;
  }
}
```

**Expected result:** Console output shows the exact order of async operations with timing information.

### 2. Wrap every async function in try-catch

Unhandled async errors are the most common cause of silent failures. When an awaited Promise rejects and there is no try-catch, the error can disappear completely in some environments. Always wrap the body of async functions in try-catch blocks. Log the full error object including the stack trace. Re-throw the error after logging if the caller needs to handle it. This ensures every async failure is visible in your Console output.

```
// BAD: error silently disappears
async function saveData(data) {
  const response = await fetch('/api/save', {
    method: 'POST',
    body: JSON.stringify(data)
  });
  return response.json();
}

// GOOD: error is caught, logged, and re-thrown
async function saveData(data) {
  try {
    const response = await fetch('/api/save', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return await response.json();
  } catch (error) {
    console.error('[saveData] Failed:', error.message);
    console.error('[saveData] Stack:', error.stack);
    throw error;
  }
}
```

**Expected result:** All async errors are logged to Console with full error messages and stack traces.

### 3. Handle unhandled Promise rejections globally

Even with try-catch in most functions, some Promises may still reject without a handler. Add a global unhandled rejection handler at the top of your entry file. In Node.js, listen for the unhandledRejection event on the process object. This catches any Promise rejection that was not handled by a .catch() or try-catch, preventing silent failures. Log the error with as much context as possible so you can trace it back to the source.

```
// Add to the top of your entry file (index.js / server.js)

// Catch unhandled Promise rejections
process.on('unhandledRejection', (reason, promise) => {
  console.error('=== UNHANDLED PROMISE REJECTION ===');
  console.error('Reason:', reason);
  console.error('Promise:', promise);
  console.error('Stack:', reason?.stack || 'No stack trace');
  console.error('===================================');
});

// Catch uncaught exceptions
process.on('uncaughtException', (error) => {
  console.error('=== UNCAUGHT EXCEPTION ===');
  console.error('Error:', error.message);
  console.error('Stack:', error.stack);
  console.error('==========================');
  process.exit(1);
});
```

**Expected result:** Any unhandled Promise rejection is logged with details instead of being silently swallowed.

### 4. Debug concurrent async operations

When running multiple async operations in parallel with Promise.all, a single failure rejects the entire batch and you lose results from the operations that succeeded. Use Promise.allSettled instead to see results and errors from every operation. Log each result's status (fulfilled or rejected) to identify which specific operation failed. This is crucial for debugging batch API calls, parallel database queries, or simultaneous file operations.

```
// Promise.all — one failure cancels everything (bad for debugging)
try {
  const [users, orders, products] = await Promise.all([
    fetchUsers(),
    fetchOrders(),
    fetchProducts()
  ]);
} catch (error) {
  console.error('Something failed, but what?', error.message);
}

// Promise.allSettled — see every result (good for debugging)
const results = await Promise.allSettled([
  fetchUsers(),
  fetchOrders(),
  fetchProducts()
]);

results.forEach((result, index) => {
  const names = ['fetchUsers', 'fetchOrders', 'fetchProducts'];
  if (result.status === 'fulfilled') {
    console.log(`[${names[index]}] SUCCESS:`, result.value.length, 'items');
  } else {
    console.error(`[${names[index]}] FAILED:`, result.reason.message);
  }
});
```

**Expected result:** Each parallel operation logs its own success or failure independently.

### 5. Use Replit Console and Preview DevTools for different log types

Replit has two separate places where logs appear. The Console pane shows output from your server-side code — anything logged with console.log in your Node.js or Express server appears here when you click Run. The Preview DevTools (accessible by right-clicking the preview pane and selecting Inspect, or clicking the DevTools icon) shows client-side JavaScript logs from your React or frontend code. If you cannot find your logs, you may be looking in the wrong pane.

```
// Server-side — appears in Replit Console
app.get('/api/data', async (req, res) => {
  console.log('[SERVER] Request received for /api/data');
  // This log appears in Console pane
});

// Client-side — appears in Preview DevTools
function App() {
  useEffect(() => {
    console.log('[CLIENT] Component mounted');
    // This log appears in Preview DevTools, NOT Console
  }, []);
}
```

**Expected result:** You know where to find server-side logs (Console) and client-side logs (Preview DevTools).

### 6. Debug async Python code with asyncio debug mode

For Python projects, enable asyncio debug mode to get detailed warnings about slow coroutines, unawaited coroutines, and other async issues. Set the PYTHONASYNCIODEBUG environment variable to 1 in your Secrets, or enable it in code. Use the logging module instead of print for structured output with timestamps and severity levels. Python's asyncio also warns about coroutines that were created but never awaited, which is a common bug.

```
import asyncio
import logging

# Enable asyncio debug mode
logging.basicConfig(level=logging.DEBUG)

async def fetch_data(url):
    logging.info(f'[fetch_data] Starting request to {url}')
    try:
        # Simulate async HTTP request
        await asyncio.sleep(1)
        logging.info(f'[fetch_data] Completed request to {url}')
        return {'status': 'ok'}
    except Exception as e:
        logging.error(f'[fetch_data] Failed: {e}')
        raise

async def main():
    logging.info('[main] Starting async operations')
    tasks = [
        fetch_data('https://api.example.com/users'),
        fetch_data('https://api.example.com/orders'),
    ]
    results = await asyncio.gather(*tasks, return_exceptions=True)
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            logging.error(f'Task {i} failed: {result}')
        else:
            logging.info(f'Task {i} succeeded: {result}')

asyncio.run(main(), debug=True)
```

**Expected result:** Debug output shows the execution order of async tasks with timestamps, warnings, and error details.

## Complete code example

File: `debug-async.js`

```javascript
// debug-async.js — Complete async debugging setup for Replit
// Includes structured logging, error handling, and global rejection handlers

import express from 'express';

const app = express();

// === GLOBAL ERROR HANDLERS ===
process.on('unhandledRejection', (reason, promise) => {
  console.error('=== UNHANDLED PROMISE REJECTION ===');
  console.error('Reason:', reason?.message || reason);
  console.error('Stack:', reason?.stack || 'No stack trace');
});

process.on('uncaughtException', (error) => {
  console.error('=== UNCAUGHT EXCEPTION ===');
  console.error(error.message);
  console.error(error.stack);
  process.exit(1);
});

// === LOGGING UTILITY ===
function log(context, message, data = '') {
  const timestamp = new Date().toISOString();
  console.log(`[${timestamp}] [${context}] ${message}`, data);
}

function logError(context, message, error) {
  const timestamp = new Date().toISOString();
  console.error(`[${timestamp}] [${context}] ERROR: ${message}`);
  console.error(`  Message: ${error.message}`);
  console.error(`  Stack: ${error.stack}`);
}

// === ASYNC FUNCTION WITH DEBUGGING ===
async function fetchWithRetry(url, retries = 3) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    log('fetchWithRetry', `Attempt ${attempt}/${retries}`, url);
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      const data = await response.json();
      log('fetchWithRetry', `Success on attempt ${attempt}`);
      return data;
    } catch (error) {
      logError('fetchWithRetry', `Attempt ${attempt} failed`, error);
      if (attempt === retries) throw error;
      log('fetchWithRetry', `Waiting 1s before retry...`);
      await new Promise(r => setTimeout(r, 1000));
    }
  }
}

// === API ROUTE WITH ASYNC DEBUGGING ===
app.get('/api/dashboard', async (req, res) => {
  log('GET /api/dashboard', 'Request received');
  const start = Date.now();

  try {
    const results = await Promise.allSettled([
      fetchWithRetry('https://api.example.com/users'),
      fetchWithRetry('https://api.example.com/stats'),
    ]);

    const [users, stats] = results.map((r, i) => {
      if (r.status === 'fulfilled') {
        log('dashboard', `Task ${i} succeeded`);
        return r.value;
      } else {
        logError('dashboard', `Task ${i} failed`, r.reason);
        return null;
      }
    });

    const duration = Date.now() - start;
    log('GET /api/dashboard', `Completed in ${duration}ms`);
    res.json({ users, stats, duration });
  } catch (error) {
    logError('GET /api/dashboard', 'Unrecoverable error', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.listen(3000, '0.0.0.0', () => {
  log('server', 'Running on port 3000');
});
```

## Common mistakes

- **Forgetting to await an async function call, causing the Promise to be created but never resolved** — undefined Fix: Always use await before async function calls unless you intentionally want to fire-and-forget. Enable asyncio debug mode in Python or look for RuntimeWarning: coroutine was never awaited messages.
- **Looking for client-side logs in Replit Console instead of Preview DevTools** — undefined Fix: Console shows server-side output only. Client-side console.log calls from React or frontend JavaScript appear in the Preview DevTools. Open the preview in a new tab and use browser DevTools (F12).
- **Using Promise.all for debugging parallel operations and losing information about which operation failed** — undefined Fix: Switch to Promise.allSettled during debugging. It returns results for all operations including both fulfilled values and rejection reasons.
- **Not checking fetch response.ok, assuming that fetch throws on HTTP errors like 404 or 500** — undefined Fix: fetch only throws on network errors. Check response.ok or response.status after every fetch call and throw manually for non-2xx responses.
- **Leaving verbose debug logging in production, filling Console with noise and slowing the app** — undefined Fix: Use a log level system (debug, info, warn, error) and set the level to info or warn in production. Only log errors and important events in deployed apps.

## Best practices

- Add labeled console.log statements before and after every await to trace async execution order
- Wrap all async function bodies in try-catch blocks to prevent silent error swallowing
- Add global unhandledRejection and uncaughtException handlers at the top of your entry file
- Use Promise.allSettled instead of Promise.all when debugging parallel operations to see all results
- Check response.ok after every fetch call because HTTP errors (404, 500) do not throw by default
- Use Replit Console for server-side logs and Preview DevTools for client-side logs
- Include timestamps in log output to measure async operation duration and identify slow operations
- Remove or reduce debug logging before deploying to production to keep logs clean and reduce overhead

## Frequently asked questions

### Why does my async error not show up in the Replit Console?

If the error is in client-side JavaScript (React, frontend code), it appears in Preview DevTools, not Console. If it is server-side but inside an async function without try-catch, the Promise rejection may be silently swallowed. Add a global unhandledRejection handler to catch these.

### Does Replit support breakpoints for debugging?

Replit does not have a built-in breakpoint debugger like VS Code. The most effective debugging approach in Replit is structured logging with console.log and console.error. For client-side code, you can use browser DevTools breakpoints in the Preview pane.

### How do I debug a race condition in async code?

Add timestamps to every log statement and run the code multiple times. Race conditions produce different log orderings on different runs. Use Promise.allSettled to ensure all operations complete, and add explicit ordering with await if operations must run sequentially.

### What is the difference between Console and Shell for debugging?

Console shows output from your app when you click Run — it is read-only and structured. Shell is an interactive terminal where you can run commands, execute scripts, and test code snippets. Use Console for monitoring your running app and Shell for ad-hoc debugging commands.

### How do I debug async code in Python on Replit?

Enable asyncio debug mode with asyncio.run(main(), debug=True) or set the PYTHONASYNCIODEBUG=1 environment variable in Secrets. Use the logging module instead of print for structured output. Use asyncio.gather with return_exceptions=True to see all task results including failures.

### Can I use a logging library instead of console.log?

Yes. Libraries like winston (Node.js) or the built-in logging module (Python) provide structured logging with levels, timestamps, and output formatting. For production apps with complex debugging needs, RapidDev can help set up comprehensive logging and monitoring infrastructure.

### Why does my app crash with UnhandledPromiseRejection in Replit?

A Promise was rejected and no .catch() or try-catch handled it. Node.js 18+ treats unhandled rejections as fatal errors by default. Add try-catch to the async function that caused the rejection, and add a global process.on('unhandledRejection') handler as a safety net.

### How do I measure async function performance in Replit?

Record Date.now() before and after each await, then log the difference. For more precise timing, use performance.now() in Node.js. Include these timing logs during development to identify slow operations that might be causing timeouts.

---

Source: https://www.rapidevelopers.com/replit-tutorial/how-to-debug-asynchronous-code-in-replit-using-logging-and-breakpoints
© RapidDev — https://www.rapidevelopers.com/replit-tutorial/how-to-debug-asynchronous-code-in-replit-using-logging-and-breakpoints
