Grouping Logs Together with console.group() JS

Learn how to organize JavaScript console output using console.group(), console.groupCollapsed(), and console.groupEnd(). Build structured, readable debugging sessions with nested groups and real-world patterns.

JavaScriptbeginner
10 min read

When you debug a complex process, a flat stream of console.log() output quickly becomes unreadable. An authentication flow might produce 15 log messages, an API response handler another 10, and suddenly you are scrolling through dozens of unlabeled lines trying to find the one value that matters.

console.group() solves this by letting you wrap related console messages in collapsible, labeled sections. You can nest groups inside groups, collapse verbose output by default, and use console.warn() or console.error() inside groups to highlight the important lines. The result is console output that reads like an outline instead of a wall of text.

How console.group() Works

The grouping API consists of three methods:

MethodPurpose
console.group(label)Starts a new group (expanded by default)
console.groupCollapsed(label)Starts a new group (collapsed by default)
console.groupEnd()Closes the most recently opened group

Everything logged between console.group() and console.groupEnd() appears indented under the group label. In Chrome DevTools, Firefox Console, and Edge DevTools, the group label is clickable and toggles the visibility of its contents.

Basic Group Example

javascriptjavascript
console.group('User Profile Load');
console.log('Fetching user ID: 12345');
console.log('Response status: 200');
console.log('User name: Alice Chen');
console.log('Role: admin');
console.groupEnd();
 
console.group('Dashboard Data');
console.log('Loading charts...');
console.log('5 widgets initialized');
console.groupEnd();

Output in the browser console:

CodeCode
▼ User Profile Load
    Fetching user ID: 12345
    Response status: 200
    User name: Alice Chen
    Role: admin
▼ Dashboard Data
    Loading charts...
    5 widgets initialized

Each group is collapsible. Clicking "User Profile Load" hides those four lines, letting you focus on "Dashboard Data" or vice versa.

console.groupCollapsed() for Verbose Output

When debugging output is useful for reference but does not need immediate attention, console.groupCollapsed() starts the group in a collapsed state:

javascriptjavascript
async function loadProductCatalog() {
  console.group('Product Catalog');
  console.log('Fetching catalog from API...');
  
  const response = await fetch('/api/products');
  const products = await response.json();
  
  console.log(`Loaded ${products.length} products`);
  
  // Detailed per-product data — collapsed by default
  console.groupCollapsed(`Product Details (${products.length} items)`);
  products.forEach(product => {
    console.log(`${product.name}: $${product.price} (${product.stock} in stock)`);
  });
  console.groupEnd(); // Closes "Product Details"
  
  console.log('Catalog ready');
  console.groupEnd(); // Closes "Product Catalog"
  
  return products;
}

Output:

CodeCode
▼ Product Catalog
    Fetching catalog from API...
    Loaded 48 products
  ▶ Product Details (48 items)    ← collapsed, click to expand
    Catalog ready

The summary ("Loaded 48 products") is visible immediately. The 48 individual product lines are hidden behind a click. This keeps the console clean while preserving all the detail when you need it.

Nesting Groups

Groups can be nested to any depth. This maps naturally to processes that have sub-steps:

javascriptjavascript
function processCheckout(cart) {
  console.group('Checkout Process');
  
  console.group('Cart Validation');
  console.log(`Items: ${cart.items.length}`);
  console.log(`Subtotal: $${cart.subtotal}`);
  
  const invalidItems = cart.items.filter(item => item.stock === 0);
  if (invalidItems.length > 0) {
    console.warn(`${invalidItems.length} items are out of stock`);
  } else {
    console.log('All items in stock');
  }
  console.groupEnd(); // Closes "Cart Validation"
  
  console.group('Payment Processing');
  console.log(`Payment method: ${cart.paymentMethod}`);
  console.log('Contacting payment gateway...');
  console.log('Payment authorized');
  console.groupEnd(); // Closes "Payment Processing"
  
  console.group('Order Confirmation');
  console.log(`Order ID: ORD-${Date.now()}`);
  console.log('Confirmation email sent');
  console.groupEnd(); // Closes "Order Confirmation"
  
  console.groupEnd(); // Closes "Checkout Process"
}

Output:

CodeCode
▼ Checkout Process
  ▼ Cart Validation
      Items: 3
      Subtotal: $127.50
      All items in stock
  ▼ Payment Processing
      Payment method: credit_card
      Contacting payment gateway...
      Payment authorized
  ▼ Order Confirmation
      Order ID: ORD-1709546400000
      Confirmation email sent

Each sub-step is independently collapsible. You can collapse "Cart Validation" and "Order Confirmation" to focus on "Payment Processing" when debugging a payment issue.

Real-World Patterns

Pattern 1: Grouping API Request and Response

Wrap the full lifecycle of an API call in a group so the request details, timing, and response are always together:

javascriptjavascript
async function apiCall(endpoint, options = {}) {
  const method = options.method || 'GET';
  const label = `${method} ${endpoint}`;
  
  console.group(label);
  console.time(label);
  
  if (options.body) {
    console.groupCollapsed('Request Body');
    console.log(JSON.parse(options.body));
    console.groupEnd();
  }
  
  try {
    const response = await fetch(endpoint, options);
    const data = await response.json();
    
    console.log('Status:', response.status);
    console.groupCollapsed('Response Body');
    console.log(data);
    console.groupEnd();
    
    console.timeEnd(label);
    console.groupEnd();
    return data;
  } catch (error) {
    console.error('Request failed:', error.message);
    console.timeEnd(label);
    console.groupEnd();
    throw error;
  }
}
 
// Usage
await apiCall('/api/users', {
  method: 'POST',
  body: JSON.stringify({ name: 'Alice', role: 'admin' }),
  headers: { 'Content-Type': 'application/json' },
});

Output:

CodeCode
▼ POST /api/users
  ▶ Request Body               ← collapsed
    Status: 201
  ▶ Response Body              ← collapsed
    POST /api/users: 142ms

Pattern 2: Grouping Loop Iterations

When debugging a loop, group each iteration so you can collapse the ones that work correctly and focus on the problematic one:

javascriptjavascript
function processInvoices(invoices) {
  console.group(`Processing ${invoices.length} invoices`);
  
  invoices.forEach((invoice, index) => {
    const hasIssue = invoice.total <= 0 || !invoice.customer;
    const groupMethod = hasIssue ? 'group' : 'groupCollapsed';
    
    console[groupMethod](`Invoice #${invoice.id} (${invoice.customer || 'MISSING'})`);
    
    console.log('Total:', invoice.total);
    console.log('Items:', invoice.items.length);
    console.log('Date:', invoice.date);
    
    if (invoice.total <= 0) {
      console.error('Invalid total: must be positive');
    }
    if (!invoice.customer) {
      console.error('Missing customer name');
    }
    
    console.groupEnd();
  });
  
  console.groupEnd();
}

This approach automatically expands invoices with problems and collapses healthy ones. You see the errors without scrolling past hundreds of "everything is fine" lines.

Pattern 3: Event Handler Debugging

When multiple events fire in sequence, group them to see the flow:

javascriptjavascript
function attachFormDebugger(form) {
  form.addEventListener('focus', (e) => {
    console.groupCollapsed(`Focus: ${e.target.name || e.target.tagName}`);
    console.log('Element:', e.target);
    console.log('Related target:', e.relatedTarget);
    console.groupEnd();
  }, true);
  
  form.addEventListener('input', (e) => {
    console.groupCollapsed(`Input: ${e.target.name}`);
    console.log('Value:', e.target.value);
    console.log('Valid:', e.target.validity.valid);
    console.groupEnd();
  });
  
  form.addEventListener('submit', (e) => {
    e.preventDefault();
    console.group('Form Submit');
    const formData = new FormData(form);
    for (const [key, value] of formData) {
      console.log(`${key}: ${value}`);
    }
    console.groupEnd();
  });
}

Pattern 4: Conditional Debug Groups

Create a utility function that only produces grouped output in development:

javascriptjavascript
const debugGroup = {
  start(label) {
    if (process.env.NODE_ENV === 'development') {
      console.group(label);
    }
  },
  startCollapsed(label) {
    if (process.env.NODE_ENV === 'development') {
      console.groupCollapsed(label);
    }
  },
  end() {
    if (process.env.NODE_ENV === 'development') {
      console.groupEnd();
    }
  },
};
 
// Usage — groups only appear in development
debugGroup.start('Auth Flow');
console.log('Checking token...');
debugGroup.end();

Combining Groups with Other Console Methods

Groups work with every other console method. Combine them for rich, organized output:

javascriptjavascript
function auditUserPermissions(user) {
  console.group(`Permission Audit: ${user.name}`);
  
  console.table(user.permissions.map(p => ({
    resource: p.resource,
    action: p.action,
    granted: p.granted ? 'Yes' : 'No',
  })));
  
  const deniedCount = user.permissions.filter(p => !p.granted).length;
  
  if (deniedCount > 0) {
    console.warn(`${deniedCount} permissions are denied`);
  }
  
  console.assert(
    user.permissions.some(p => p.resource === 'admin'),
    'User has no admin-related permissions'
  );
  
  console.time('Policy evaluation');
  // ... evaluate complex access policies
  console.timeEnd('Policy evaluation');
  
  console.groupEnd();
}

Best Practices

Using Groups Effectively

These practices prevent console groups from becoming more cluttered than the flat output they replaced.

Always close your groups. Every console.group() or console.groupCollapsed() needs a matching console.groupEnd(). An unclosed group causes all subsequent console output to be indented inside it, which is confusing and can persist across unrelated function calls.

Use groupCollapsed for verbose data. If a group contains more than 5 lines or logs iteration details, start it collapsed. The summary should be visible at the group label level; the details should be one click away.

Use descriptive group labels. console.group('Process') tells you nothing. console.group('POST /api/orders - Processing 5 items') tells you exactly what happened. Include the entity ID, count, or key identifier in the label.

Limit nesting to 2-3 levels. Deeply nested groups are harder to read than flat output. If you need more than 3 levels, consider splitting the logic into separate functions with their own top-level groups.

Remove groups in production. Like all console output, grouped logs should be stripped from production builds. Groups are a development tool, not a logging framework.

Common Mistakes and How to Avoid Them

Group Pitfalls to Watch For

These mistakes make grouped output harder to read instead of easier.

Missing console.groupEnd() in error paths. If your function has early returns or try-catch blocks, the group might never close:

javascriptjavascript
// Bad — group stays open if fetch fails
function loadData() {
  console.group('Loading data');
  
  try {
    const data = fetchSync('/api/data');
    console.log('Loaded:', data);
  } catch (error) {
    console.error('Failed:', error);
    return; // Group is never closed!
  }
  
  console.groupEnd();
}
 
// Good — groupEnd in finally
function loadData() {
  console.group('Loading data');
  
  try {
    const data = fetchSync('/api/data');
    console.log('Loaded:', data);
  } catch (error) {
    console.error('Failed:', error);
  } finally {
    console.groupEnd(); // Always closes
  }
}

Grouping single log statements. A group with one console.log() inside it adds clutter without adding clarity. Only group when you have 3+ related messages.

Using expanded groups for high-frequency events. If you log every mousemove or scroll event in expanded groups, the console becomes unusable. Use groupCollapsed for events that fire rapidly, or add a counter and only log every Nth event.

Forgetting that groups are visual-only. console.group() has no effect on program execution or data flow. It purely organizes the console display. Do not use it as a replacement for structured logging in production.

Browser Compatibility

MethodChromeFirefoxSafariEdgeNode.js
console.group()YesYesYesYesYes (no collapse)
console.groupCollapsed()YesYesYesYesSame as group()
console.groupEnd()YesYesYesYesYes
Node.js Behavior

In Node.js, console.group() adds indentation to output but does not create collapsible sections (there is no interactive console). console.groupCollapsed() behaves identically to console.group() in Node.js.

Next Steps

Practice grouping in a real debugging session

Take a feature you are currently working on and add console.group() around its main operations. Observe how quickly you can isolate problems compared to flat log output.

Learn browser debugging beyond the console

Move beyond console methods by learning basic JavaScript debugging tips, including breakpoints and the DevTools Sources panel.

Master stack traces for error diagnosis

When console groups reveal an error, learn to read JavaScript stack traces to trace it back to the root cause.

Build a reusable debug logger

Create a utility module that wraps console.group() with environment checks, automatic timing, and structured labels. This becomes your team's standard debugging interface.

Rune AI

Rune AI

Key Insights

  • Three methods: console.group() opens expanded, console.groupCollapsed() opens collapsed, console.groupEnd() closes the current group
  • Descriptive labels are essential: include the operation name, entity ID, or item count in the group label for instant context
  • Use groupCollapsed for verbose output: keep summaries visible and hide details behind one click
  • Always close groups: use finally blocks to prevent unclosed groups from indenting all subsequent output
  • Development only: strip all console group calls from production builds just like any other console method
RunePowered by Rune AI

Frequently Asked Questions

Does console.group() affect performance?

In production, yes. Each group call performs I/O to the console output stream. In development, the overhead is negligible. As with all console methods, strip group calls from production builds using build tools or ESLint rules.

Can I use console.group() in Node.js?

Yes, but with limitations. Node.js supports `console.group()` and adds indentation to the output, but there is no collapsible behavior since Node.js does not have an interactive console like browser DevTools. `console.groupCollapsed()` works identically to `console.group()` in Node.js.

How deep can I nest console groups?

There is no hard limit defined by the specification, but beyond 3-4 levels the indentation becomes difficult to read. If your groups are nested 5+ levels deep, consider restructuring your debug output or splitting it into separate top-level groups.

What happens if I forget console.groupEnd()?

ll subsequent console output will be indented inside the unclosed group. This affects every `console.log()`, `console.warn()`, and `console.error()` call that runs afterward, even from unrelated functions. Always use `finally` blocks or wrapper functions to guarantee groups are closed.

Can I style console group labels?

Yes. You can use `%c` CSS styling in group labels: `console.group('%cStyled Group', 'color: blue; font-weight: bold;')`. This works in Chrome and Firefox DevTools but may not render in all environments.

Conclusion

console.group() transforms flat, scrollable debugging output into organized, collapsible sections that match the logical structure of your code. By grouping API calls, loop iterations, event handlers, and multi-step workflows, you can collapse the noise and focus on the specific area where a bug lives. Combined with console.groupCollapsed() for verbose data and nested groups for sub-steps, this method turns the browser console into a structured debugging interface.