# How to Debug JavaScript Code in Retool

- Tool: Retool
- Difficulty: Advanced
- Time required: 20-30 min
- Compatibility: Retool Cloud and Self-hosted
- Last updated: March 2026

## TL;DR

To debug JavaScript in Retool, use console.log() in JS Queries (visible in the Debug Panel console tab), inspect query timing and errors in the Debug Panel waterfall, and check component/state values in the State Inspector. For network errors, open browser DevTools and filter by your API domain. The State Inspector (bottom-left eye icon) shows real-time values of all components, queries, and state variables.

## JavaScript Debugging Tools in Retool

Debugging JavaScript in Retool is different from debugging in a standard Node.js or browser environment. Retool provides its own Debug Panel — accessible via the bug icon in the bottom toolbar — which shows a console for log output, a waterfall view of all query executions, and timing information. This is your first stop for any JS issue.

The State Inspector (eye icon, bottom-left) is the second essential tool. It shows the current value of every component, query result, state variable, and global variable in real time. When a {{ }} binding is not displaying what you expect, the State Inspector tells you exactly what value the binding is receiving.

For errors that reach the browser (uncaught exceptions, network failures, or CORS errors), the browser DevTools console and Network tab provide the most detailed information. This tutorial walks through all three debugging environments with practical examples.

## Before you start

- A Retool account with editor access to an app
- Familiarity with writing JS Queries in Retool
- Basic understanding of JavaScript async/await and Promises
- Browser DevTools familiarity (Chrome or Firefox recommended)

## Step-by-step guide

### 1. Add console.log statements to your JS Query

Open the JS Query you want to debug. Add console.log() calls at key points in your logic — after data is fetched, before a condition branches, and at the end to confirm the return value. Retool captures all console.log output from JS Queries and displays it in the Debug Panel console tab. You can log any JavaScript value including objects, arrays, and query results.

```
// JS Query: processOrders — adding debug logging
const orders = getOrders.data;
console.log('Orders loaded:', orders?.length, 'rows');
console.log('First order:', JSON.stringify(orders?.[0], null, 2));

if (!orders || orders.length === 0) {
  console.log('DEBUG: No orders found, returning empty array');
  return [];
}

const filtered = orders.filter(o => o.status === 'pending');
console.log('Pending orders:', filtered.length);

const result = filtered.map(o => ({ ...o, priority: o.value > 1000 ? 'high' : 'normal' }));
console.log('Final result sample:', result[0]);
return result;
```

**Expected result:** After running the query, switch to the Debug Panel (bug icon) → Console tab to see all log output with timestamps.

### 2. Open and read the Debug Panel waterfall

Click the bug icon in the bottom toolbar to open the Debug Panel. Switch to the Waterfall tab (or Timeline). Every query execution appears here — SQL queries, REST API calls, JS queries, and transformers — with their start time, duration, status (success/failure), and payload size. Look for red entries (failures), yellow entries (warnings), or unexpectedly long durations. Click any entry to see the full request and response.

**Expected result:** The waterfall shows a timeline of all query executions. Failed queries appear in red with their error message.

### 3. Inspect component and variable values with the State Inspector

Click the eye icon in the bottom-left corner of the Retool editor to open the State Inspector. This panel shows a live JSON tree of every component's current value, every query's data/error/isFetching state, every state variable's current value, and every global variable. When a {{ }} expression is not rendering what you expect, find the referenced component or query in the State Inspector to see its actual current value.

```
// In the State Inspector, you will see:
// Components section:
//   table1:
//     selectedRow: { data: { id: 42, name: 'Alice', status: 'active' } }
//     data: [{ id: 42, ...}, { id: 43, ...}]
// Queries section:
//   getOrders:
//     data: [{ id: 1, ... }]
//     error: null
//     isFetching: false
// State Variables section:
//   filterState: { value: 'pending' }
//   isModalOpen: { value: false }
```

**Expected result:** The State Inspector shows the live values of all components and variables, helping identify why a binding is showing unexpected data.

### 4. Debug async errors and Promise rejections

Async errors in Retool JS Queries often manifest as silent failures — the query appears to complete but nothing happens. This happens when an awaited call throws but you do not have a try/catch. Wrap your async logic in try/catch to capture these errors and log them. Also check whether you are missing await keywords, which causes the query to return before async operations complete.

```
// PROBLEM: Silent failure — missing try/catch
async function badExample() {
  const result = await riskyQuery.trigger(); // Throws, but no catch!
  return result.data;
}

// FIXED: Wrap in try/catch with logging
try {
  console.log('Starting async operation...');
  const result = await riskyQuery.trigger();
  console.log('Query succeeded:', result);
  return result.data;
} catch (err) {
  console.error('Query failed:', err.message);
  utils.showNotification({
    title: 'Operation failed',
    description: err.message,
    notificationType: 'error'
  });
  return null;
}

// COMMON MISTAKE: Missing await causes race condition
// WRONG:
state1.setValue('loading', true);
const data = heavyQuery.trigger(); // Returns Promise, not data!
console.log(data); // Promise { <pending> }

// CORRECT:
await state1.setValue('loading', true);
const data = await heavyQuery.trigger();
console.log(data); // Actual result
```

**Expected result:** Errors are caught and logged, making the failure visible in the Debug Panel console instead of failing silently.

### 5. Use browser DevTools for network and runtime errors

For errors that Retool's Debug Panel does not capture — CORS errors, network failures, or JavaScript runtime exceptions — open browser DevTools (F12 or Cmd+Option+I in Chrome). Check the Console tab for red error messages. Check the Network tab and filter by XHR/Fetch to see the actual HTTP requests Retool makes to your resources. Look for 4xx/5xx responses and click them to see the full request/response details.

**Expected result:** Browser DevTools shows the full HTTP request/response for failed API calls, including status codes, headers, and response body.

### 6. Add a global error handler pattern

For production apps, add a global try/catch wrapper in every JS Query that makes external calls. Create a reusable pattern where errors are always logged to the console and shown to the user via notification. This prevents silent failures and gives you a consistent debugging surface.

```
// Reusable pattern for all JS Queries that call external resources
const QUERY_NAME = 'fetchUserData'; // For debugging identification

try {
  // Your query logic here
  const users = await getUsers.trigger();
  console.log(`[${QUERY_NAME}] Fetched ${users.data?.length} users`);
  
  const processed = users.data.map(u => ({
    ...u,
    displayName: `${u.first_name} ${u.last_name}`
  }));
  
  return processed;
} catch (err) {
  const message = err?.message || 'Unknown error';
  console.error(`[${QUERY_NAME}] FAILED:`, message, '\nStack:', err?.stack);
  
  utils.showNotification({
    title: `Error in ${QUERY_NAME}`,
    description: message,
    notificationType: 'error',
    duration: 5000
  });
  
  return []; // Return safe default
}
```

**Expected result:** Every error produces both a console log entry (for debugging) and a user-visible notification, with no silent failures.

## Complete code example

File: `JS Query: debuggableDataProcessor`

```javascript
// Fully debuggable JS Query pattern
// Demonstrates: console.log, try/catch, async/await, early returns

const QUERY = 'processCustomerReport';
console.log(`[${QUERY}] Starting execution`);
console.log(`[${QUERY}] Inputs: search='${searchInput.value}', filter='${statusFilter.value}'`);

// Input validation with early return
if (!searchInput.value && !statusFilter.value) {
  console.log(`[${QUERY}] No filters applied — returning all data`);
}

try {
  // Check if source query has data
  if (!getCustomers.data) {
    console.warn(`[${QUERY}] getCustomers.data is null — query may not have run`);
    return [];
  }
  
  console.log(`[${QUERY}] Source data: ${getCustomers.data.length} rows`);
  console.log(`[${QUERY}] Sample row:`, JSON.stringify(getCustomers.data[0], null, 2));

  // Filter step
  let result = getCustomers.data;
  
  if (statusFilter.value) {
    result = result.filter(c => c.status === statusFilter.value);
    console.log(`[${QUERY}] After status filter (${statusFilter.value}): ${result.length} rows`);
  }
  
  if (searchInput.value) {
    const term = searchInput.value.toLowerCase();
    result = result.filter(c =>
      c.name?.toLowerCase().includes(term) ||
      c.email?.toLowerCase().includes(term)
    );
    console.log(`[${QUERY}] After search filter ('${term}'): ${result.length} rows`);
  }

  // Transform step
  const transformed = result.map(c => ({
    id: c.id,
    name: c.name,
    email: c.email,
    status: c.status,
    revenue: c.total_orders * c.avg_order_value,
    last_seen: moment(c.last_active).fromNow()
  }));

  console.log(`[${QUERY}] Final result: ${transformed.length} rows`);
  return transformed;

} catch (err) {
  console.error(`[${QUERY}] FAILED:`, err.message, err.stack);
  utils.showNotification({
    title: 'Report error',
    description: err.message,
    notificationType: 'error'
  });
  return [];
}
```

## Common mistakes

- **JS Query appears to complete successfully but returns nothing — no visible output** — undefined Fix: Add console.log at the end of the query to confirm it is returning the expected value. The most common cause is a missing await before a .trigger() call, so the query finishes before the async operation completes.
- **Console.log in a transformer is not appearing in the Debug Panel** — undefined Fix: Transformers run in a separate context and their console output may not appear in the Debug Panel. Move the logging into a JS Query instead, or check the browser DevTools console directly.
- **Cannot find the error — query shows as 'failed' but no error message is visible** — undefined Fix: Click the failed query entry in the Debug Panel waterfall to expand it. The full error message and stack trace appear in the expanded view. If still unclear, open browser DevTools Network tab and look for failed HTTP requests.
- **Using debugger breakpoints and the code never pauses** — undefined Fix: Retool JS Queries run in a sandboxed iframe context. Native browser debugger breakpoints do not work in this context. Use console.log() for all debugging — it is the only reliable debugging tool inside JS Queries.

## Best practices

- Always wrap async JS Queries in try/catch — unhandled Promise rejections fail silently and are extremely hard to debug.
- Add a query identifier prefix to console.log messages (e.g., [queryName]) so you can filter logs when multiple queries are running.
- Use the State Inspector before adding console.log — often the issue is visible immediately in the component's current value.
- Log the query inputs at the top of every JS Query so you can verify the values entering your logic.
- Check query1.isFetching and query1.error in the State Inspector before assuming query data is available.
- Use JSON.stringify(obj, null, 2) for readable object logging — the Debug Panel collapses nested objects by default.
- For production apps, remove or gate verbose console.log calls behind a debug flag state variable to avoid exposing sensitive data in the browser console.

## Frequently asked questions

### Where do console.log outputs appear in Retool?

Console.log outputs from JS Queries appear in the Debug Panel, which you open by clicking the bug icon in the bottom toolbar. Switch to the Console tab to see log output. Retool also forwards these logs to the browser DevTools console, but the Debug Panel is more convenient during development.

### Can I use breakpoints or step-through debugging in Retool JS Queries?

No. Retool JS Queries run inside a sandboxed iframe, which means browser debugger breakpoints and DevTools Sources panel breakpoints do not work. The only available debugging tool is console.log(). For complex logic, break it into smaller functions and log the output of each step.

### What does 'isFetching: true' mean for a query in the State Inspector?

isFetching: true means the query is currently executing and has not returned a result yet. During this state, query.data holds the result from the previous execution (or null on first run). If you are reading query.data immediately after triggering a query without await, you are reading stale data. Use await query.trigger() to wait for completion.

---

Source: https://www.rapidevelopers.com/retool-tutorial/how-to-debug-javascript-code-in-retool
© RapidDev — https://www.rapidevelopers.com/retool-tutorial/how-to-debug-javascript-code-in-retool
