WordPress hooks are the backbone of extensible development. Actions let you run code at a specific moment. Filters let you modify data before it is used. The concept is simple – the execution is where developers trip up.
After reviewing hundreds of plugin codebases and WordPress projects, the same mistakes appear again and again. Some of them break sites in production. Some create subtle bugs that take days to track down. This article covers 9 of the most common hook mistakes, what the wrong pattern looks like, and what the correct approach is.
Mistake 1: Wrong Priority – Your Hook Never Runs
Every hook callback registers with a priority. The default is 10. WordPress runs callbacks in ascending priority order – lower numbers run first. The mistake happens when you try to hook into something that another plugin already ran at the same or higher priority, or when you expect to override a core callback but register at the same level.
A real example: WooCommerce fires woocommerce_before_cart and most of its internal callbacks run at priority 10. If you add yours at 10, execution order is unpredictable – it depends on which code loaded first. If the thing you need to modify already ran, your callback is too late.
Not This
Do This
When you need to run after another callback, use a higher priority number. When you need to run before, use a lower one. If you are removing a core or plugin callback, check its registered priority with has_action() first – then use the exact same value to remove it or a lower one to override before it runs.
The default priority of 10 is not “normal” – it means you are competing with everyone else who also did not think about priority.
Mistake 2: The remove_action Gotcha with Class Instances
This is one of the most frustrating WordPress bugs to debug. You call remove_action() correctly – right hook name, right priority – but the callback keeps firing. The reason: you are passing a new object instance, not the original one that registered the hook.
When a class instance registers a hook like add_action( 'init', [ $this, 'method_name' ] ), the callback is bound to that specific object in memory. Passing a brand new instance to remove_action() does not match it because PHP compares object identity, not just class name.
Not This
Do This
The correct approach depends on how the class is instantiated. If the plugin uses a singleton or stores its instance in a global variable, access that variable. If the plugin registers hooks inside a static method, pass the class name as a string instead of an object. If neither applies, use the $wp_filter global directly to locate and remove the callback.
- Check if the plugin uses
Plugin::get_instance()or a global like$GLOBALS['plugin_name'] - Use
has_action( 'hook', [ $instance, 'method' ] )to verify you have the right reference before removing - As a last resort, inspect
$wp_filter['hook_name']to see what is registered and at what priority
Mistake 3: Using Anonymous Functions You Can Never Remove
Closures and anonymous functions look clean in modern PHP. The problem is you cannot remove them later. remove_action() and remove_filter() work by matching the exact callback reference. With anonymous functions, there is no reference to pass back.
This matters most in plugins and themes that others might extend. If you use an anonymous function, no one can unhook it without digging into the $wp_filter global – which is fragile and version-dependent.
Not This
Do This
Named functions and class methods can always be removed by passing the same reference. If you want to keep the closure syntax for readability, assign it to a variable first and store that variable somewhere accessible – but this is still messier than a named method on a class. In plugin development, always prefer named callbacks.
If you cannot unhook it, you should not hook it with an anonymous function.
Mistake 4: Late Binding – Hooking After the Action Already Fired
WordPress runs actions once during a request. If you add a callback after an action has already fired, your callback never runs in that request. This is a silent failure – no PHP error, no warning, nothing in the log. Your code just does not execute.
Common scenario: registering hooks inside a class constructor that runs too late, or conditionally hooking based on a value that is only available after the relevant action has passed.
Not This
Do This
Know the WordPress load order. Actions fire in a predictable sequence: muplugins_loaded then plugins_loaded then setup_theme then after_setup_theme then init then wp_loaded then wp then template_redirect then wp_head. Register your hooks at the earliest action where the data you need is available, not the latest one that feels convenient.
| Action | What is Available | Use For |
|---|---|---|
| plugins_loaded | All plugins loaded | Plugin compatibility checks |
| init | User authenticated, query set up | Post types, taxonomies, rewrite rules |
| wp_loaded | Full WordPress loaded | AJAX handlers, REST routes |
| template_redirect | Query determined, template not chosen | Redirects, conditional output |
| wp_enqueue_scripts | Front-end context confirmed | Scripts and styles |
Mistake 5: Conditional Hooks at the Wrong Timing
Wrapping add_action() inside a conditional like is_admin(), is_singular(), or current_user_can() seems logical. Run this hook only on admin pages, or only for editors. The problem is timing – many of these conditional functions are not reliable until after a specific action has fired.
is_admin() works fine early in the load process. But is_singular(), is_page(), is_category(), and most other query-based conditionals are NOT reliable until after parse_query runs – which is after init. Checking them at plugins_loaded or inside a function file will return false or unpredictable results.
Not This
Do This
Register the hook unconditionally on the early action. Move the conditional check inside the callback itself, where it runs at the right time. This is the difference between “should this hook be registered” (early, often wrong) and “should this hook do anything right now” (inside callback, always right).
- is_admin() – safe to check at plugins_loaded or init
- is_singular(), is_page(), is_archive() – only safe inside callbacks at template_redirect or later
- current_user_can() – safe after init, risky before
- is_user_logged_in() – safe at init and after
Mistake 6: Filter Callbacks That Do Not Return a Value
This one is responsible for more broken sites than almost anything else on this list. A filter callback that does not explicitly return the value it received will return null – and WordPress will use null as the filtered value. The result depends on what the filter modifies. Best case: an empty string where content should be. Worst case: a fatal error because code downstream expects an array and gets null.
The mistake is especially common when developers refactor an action callback into a filter callback – actions do not need return values, filters always do.
Not This
Do This
The rule is simple: every filter callback must return a value. If you only want to run side effects (like logging or updating an option based on the filtered value), use an action instead. If you must use a filter and have no modification to make, return the original value unchanged. Never let a filter callback fall through without a return statement.
Actions do something. Filters change something. If your filter does not return, it is not a filter – it is a site-breaking action pretending to be one.
Mistake 7: Over-Hooking init – Putting Everything in One Place
The init action runs early, it runs on every request, and it feels like a safe place to put everything. That reasoning leads to bloated init callbacks that register post types, enqueue scripts, set up AJAX handlers, check user roles, and fire API calls all in one function.
The problem is not that init is wrong for these things – some of them belong there. The problem is using it as a catch-all instead of choosing the most specific hook for each task. Enqueuing scripts at init means they load on every request including AJAX and REST. AJAX handlers registered at init work but miss the more specific wp_ajax_{action} hooks. Running database queries at init on every page load is an unnecessary performance hit.
Not This
Do This
Match the hook to the task. Post types and taxonomies belong at init. Scripts and styles belong at wp_enqueue_scripts (front-end) or admin_enqueue_scripts (admin). REST routes belong at rest_api_init. Admin menus belong at admin_menu. AJAX handlers at wp_ajax_*. Using the correct hook makes your code more readable, reduces unnecessary execution, and plays better with other plugins.
Mistake 8: Not Checking if a Function Exists Before Hooking
Your plugin depends on another plugin being active. You call its functions directly inside a hook callback. When that plugin is deactivated, PHP throws a fatal error on every page load. The site goes down. This is a classic plugin dependency conflict and it is entirely preventable.
The same issue appears with WordPress functions themselves – using a function added in WordPress 6.1 without checking the version means sites on older installs break silently or fatally.
Not This
Do This
Three patterns to use depending on the situation:
- function_exists() – check before calling any external function
- class_exists() – check before instantiating or extending a class from another plugin
- defined() – many plugins define a constant when active; check that constant as a proxy for plugin availability
For more complex dependencies, consider using plugins_loaded to check if the dependency is active before registering any hooks at all. If the dependency is missing, degrade gracefully – show an admin notice, not a fatal error.
Mistake 9: Using the Wrong Hook for the Job
Not all early-loading hooks are equivalent. Not all late-loading hooks are equivalent. Using init when you need template_redirect, or using wp_head when you need wp_enqueue_scripts, creates subtle bugs that only surface in specific contexts.
The most common wrong-hook situations:
- Using
initfor redirects – redirects atinitfire before query parsing, so you cannot checkis_page()oris_singular(). Usetemplate_redirectinstead. - Using
wp_headto enqueue scripts – scripts added atwp_headbypass WordPress dependency management. Scripts enqueued atwp_enqueue_scriptsare handled correctly with deduplication and dependency resolution. - Using
initfor REST API registration – REST routes added atinitwork in some cases but the correct hook isrest_api_init. Using the correct hook ensures routes only register when the REST API is actually initializing. - Using
save_postfor calculations that need the post fully saved –save_postfires during the save. If you need data from a related post that is also being updated in the same request, usewp_after_insert_postwhich fires after all post metadata is saved.
Not This
Do This
Read the WordPress Developer Reference for each hook you use. The description tells you exactly when it fires, what is available at that point, and what it is intended for. Five minutes reading the docs saves hours debugging wrong-hook problems.
Quick Reference: Hook Mistakes at a Glance
| Mistake | Wrong | Correct |
|---|---|---|
| Priority | Default 10, conflicts with others | Check existing priorities, use specific values |
| remove_action class | new ClassName() | Original instance or global reference |
| Anonymous functions | add_action with anonymous function | Named function or class method |
| Late binding | Hook registered after action fired | Register at the earliest appropriate action |
| Conditional wrapping | is_page() around add_action at init | Condition check inside the callback |
| Filter return | No return statement in callback | Always return – original or modified value |
| init overuse | Scripts, AJAX, REST all in init | wp_enqueue_scripts, wp_ajax_*, rest_api_init |
| Function exists check | Direct call to external function | function_exists() or class_exists() guard |
| Wrong hook | Redirects at init, scripts at wp_head | template_redirect, wp_enqueue_scripts |
Debugging Hook Issues
When a hook is not behaving as expected, these tools and techniques narrow the problem fast:
- has_action() / has_filter() – returns the priority of a registered callback, or false if not registered. Use this to confirm your hook is actually registered.
- did_action() – returns the number of times an action has fired. If it returns 0 at the point you are checking, the action has not run yet.
- $wp_filter global – a full registry of all registered hooks. Inspect it with var_dump to see what is registered and at what priority.
- Query Monitor plugin – shows all hooks fired during a request with timing data. Essential for diagnosing load-order issues.
- WordPress core action log – add a temporary hook on ‘all’ that logs every tag via error_log, then check your server error log to see the sequence.
Developing Hook-Safe Code From the Start
The developers who avoid these mistakes are not more talented – they have just adopted a few habits that prevent them:
- Always name your callbacks. Named functions and class methods can always be removed, referenced, and debugged. Anonymous functions cannot.
- Always return from filters. Make it a habit – write the return statement before you write the function body.
- Check priority before removing. Use
has_action()to confirm the registered priority before callingremove_action(). - Use the most specific hook for each task. Do not use
initbecause it works. Use the hook that was built for what you are doing. - Guard external dependencies. Wrap every call to an external plugin or uncommon WordPress function in an existence check.
- Put conditions inside callbacks, not around them. Register hooks unconditionally on early actions. Check context inside the callback.
These are not advanced techniques – they are baseline habits. Following them consistently means the hook mistakes in this article never become your debugging sessions.
Stay Current With WordPress Best Practices
WordPress hooks evolve. New actions and filters are added with each major release. Old patterns get deprecated. The best way to stay ahead is to read the Developer Notes for each WordPress release and follow the official changelog.
TweaksWP covers exactly this – advanced WordPress techniques, performance optimizations, and debugging patterns that are not in the beginner guides. Subscribe below to get posts like this directly in your inbox. No noise, just practical WordPress development content.
Last modified: March 6, 2026