Skip to main content

Debugging

Preact provides a powerful debugging addon that helps catch common mistakes and provides detailed error messages during development. The preact/debug package enhances your development experience with helpful warnings, prop type checking, and detailed component stack traces.
The debug addon should only be imported in development environments. Never include it in production builds as it adds overhead and exposes detailed error information.

Enabling Debug Mode

To enable debug mode, import preact/debug at the entry point of your application:
import 'preact/debug';
import { render } from 'preact';
import App from './App';

render(<App />, document.getElementById('app'));
Import preact/debug as early as possible in your application to catch errors throughout your component tree.
With your bundler, you can conditionally import debug mode:
if (process.env.NODE_ENV !== 'production') {
  require('preact/debug');
}

Error Detection

The debug addon catches many common mistakes and provides detailed error messages:

Undefined Components

When you pass undefined to createElement, debug mode provides a helpful error:
// Forgot to export component
import { ComponentThatDoesntExist } from './components';

// Error: Undefined component passed to createElement()
// You likely forgot to export your component or might have 
// mixed up default and named imports
<ComponentThatDoesntExist />
Source: debug/src/debug.js:153-159

Invalid Component Types

Debug mode detects when you accidentally pass JSX literals or invalid types:
// Accidentally passing a JSX literal twice
const MyComponent = <div>Hello</div>;
const vnode = <MyComponent />; // Error!

// Error: Invalid type passed to createElement()
// Did you accidentally pass a JSX literal as JSX twice?
Source: debug/src/debug.js:160-176

Invalid Refs

Refs must be functions or objects created by createRef():
// Invalid: string ref
<div ref="myRef" /> // Error!

// Valid: function ref
<div ref={el => this.myRef = el} />

// Valid: ref object
const myRef = createRef();
<div ref={myRef} />
Source: debug/src/debug.js:178-190

Event Handler Validation

Event handlers must be functions:
// Invalid: non-function event handler
<button onClick="handleClick"> // Error!

// Valid
<button onClick={handleClick}>
Source: debug/src/debug.js:192-208

Render Loop Detection

Debug mode prevents infinite render loops by tracking consecutive renders:
function BadComponent() {
  const [state, setState] = useState(0);
  
  // This creates an infinite loop!
  setState(state + 1);
  
  return <div>{state}</div>;
}

// Error: Too many re-renders. This is limited to prevent an 
// infinite loop which may lock up your browser.
The limit is set to 25 consecutive renders. Source: debug/src/debug.js:262-269

HTML Nesting Validation

Debug mode validates proper HTML nesting according to web standards:

Table Element Nesting

// Error: Improper nesting
<table>
  <tr> // Missing <tbody>
    <td>Cell</td>
  </tr>
</table>

// Valid
<table>
  <tbody>
    <tr>
      <td>Cell</td>
    </tr>
  </tbody>
</table>
Source: debug/src/debug.js:358-440

Paragraph Element Validation

// Error: Invalid children in <p>
<p>
  <div>Block element not allowed</div>
</p>

// Valid
<p>
  <span>Inline element is fine</span>
</p>
Source: debug/src/debug.js:416-428

Interactive Content Nesting

// Error: Nested interactive elements
<a href="/link">
  <a href="/nested">Nested link</a> // Not allowed!
</a>

<button>
  <button>Nested button</button> // Not allowed!
</button>
Source: debug/src/debug.js:429-439

PropTypes Checking

Debug mode automatically checks PropTypes when defined on components:
import PropTypes from 'prop-types';

function MyComponent({ name, age }) {
  return <div>{name} is {age} years old</div>;
}

MyComponent.propTypes = {
  name: PropTypes.string.isRequired,
  age: PropTypes.number.isRequired
};

// Error: Failed prop type: Invalid prop `age` of type `string` 
// supplied to `MyComponent`, expected `number`
<MyComponent name="John" age="25" />
Source: debug/src/debug.js:210-242, debug/src/check-props.js:24-54

Component Stack Traces

Debug mode provides detailed component stack traces showing where errors occurred:
function Parent() {
  return <Child />;
}

function Child() {
  return <Broken />; // Undefined component
}

// Error output includes:
//   in Child (at Child.js:5)
//   in Parent (at Parent.js:2)

Enabling Source Locations

For detailed file and line information, add the Babel plugin:
npm install --save-dev @babel/plugin-transform-react-jsx-source
// .babelrc
{
  "plugins": [
    ["@babel/plugin-transform-react-jsx-source"]
  ]
}
Without the jsx-source plugin, stack traces will show component names but not file locations.
Source: debug/src/component-stack.js:78-100

Hook Validation

Debug mode validates hook usage:

Hook Call Location

// Error: Hook can only be invoked from render methods
const [state, setState] = useState(0); // Outside component!

function MyComponent() {
  return <div>Valid</div>;
}
Source: debug/src/debug.js:274-280

NaN in Dependencies

function MyComponent() {
  const value = NaN;
  
  useEffect(() => {
    // ...
  }, [value]); // Warning: Invalid argument passed to hook
  
  return <div>Component</div>;
}
Source: debug/src/debug.js:470-490

Lifecycle Warnings

Debug mode warns about incorrect lifecycle usage:

setState in Constructor

class MyComponent extends Component {
  constructor(props) {
    super(props);
    this.setState({ foo: 'bar' }); // Warning!
    // Use: this.state = { foo: 'bar' }; instead
  }
}
Source: debug/src/debug.js:494-511

forceUpdate on Unmounted Components

class MyComponent extends Component {
  componentWillUnmount() {
    setTimeout(() => {
      this.forceUpdate(); // Warning: memory leak!
    }, 1000);
  }
}

// Warning: Can't call "this.forceUpdate" on an unmounted component.
// This is a no-op, but it indicates a memory leak in your application.
Source: debug/src/debug.js:528-546

Suspense Validation

Debug mode ensures Suspense boundaries are properly configured:
function MyComponent() {
  throw Promise.resolve(); // No Suspense boundary!
  return <div>Content</div>;
}

// Error: Missing Suspense. The throwing component was: MyComponent

// Fix: Wrap in Suspense
<Suspense fallback={<div>Loading...</div>}>
  <MyComponent />
</Suspense>
Source: debug/src/debug.js:78-99

Duplicate Keys Warning

Debug mode detects duplicate keys in lists:
function List() {
  const items = ['a', 'b', 'a']; // Duplicate 'a'
  
  return (
    <ul>
      {items.map(item => (
        <li key={item}>{item}</li> // Warning!
      ))}
    </ul>
  );
}

// Warning: Following component has two or more children with the 
// same key attribute: "a". This may cause glitches and misbehavior 
// in rendering process.
Source: debug/src/debug.js:446-468

Hydration Mismatch Detection

When using SSR with hydration, debug mode detects mismatches:
// Server renders:
<div>Server content</div>

// Client hydrates:
<span>Client content</span>

// Error: Expected a DOM node of type "span" but found "div" 
// as available DOM-node(s), this is caused by the SSR'd HTML 
// containing different DOM-nodes compared to the hydrated one.
Source: debug/src/debug.js:582-590

Exported Utilities

The debug package exports several utilities for advanced use cases:

resetPropWarnings()

Reset the history of PropTypes warnings (useful in tests):
import { resetPropWarnings } from 'preact/debug';

beforeEach(() => {
  resetPropWarnings();
});
Source: debug/src/index.js:6, debug/src/check-props.js:8-10

Component Stack Utilities

import {
  captureOwnerStack,
  getCurrentVNode,
  getDisplayName,
  getOwnerStack,
  setupComponentStack
} from 'preact/debug';

// Get current rendering component
const vnode = getCurrentVNode();

// Get component name
const name = getDisplayName(vnode);

// Get full component stack
const stack = getOwnerStack(vnode);
Source: debug/src/index.js:8-14

Disabling in Production

Always exclude debug from production builds:

Webpack

// webpack.config.js
module.exports = {
  resolve: {
    alias: {
      'preact/debug': process.env.NODE_ENV === 'production' 
        ? 'preact/debug/noop' 
        : 'preact/debug'
    }
  }
};

Vite

// vite.config.js
export default {
  resolve: {
    alias: {
      'preact/debug': process.env.NODE_ENV === 'production'
        ? 'preact/debug/noop'
        : 'preact/debug'
    }
  }
};

Performance Impact

The debug addon adds significant overhead:
  • Validates all vnodes before and after rendering
  • Checks HTML nesting rules
  • Validates PropTypes on every render
  • Maintains component stack traces
  • Adds ~10-20KB to bundle size
Never ship debug mode to production. It will significantly slow down your application and increase bundle size.

Build docs developers (and LLMs) love