Written by 12:10 am Blog Views: 1

9 WordPress Hook Mistakes That Break Sites: Actions and Filters Done Right

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.

ActionWhat is AvailableUse For
plugins_loadedAll plugins loadedPlugin compatibility checks
initUser authenticated, query set upPost types, taxonomies, rewrite rules
wp_loadedFull WordPress loadedAJAX handlers, REST routes
template_redirectQuery determined, template not chosenRedirects, conditional output
wp_enqueue_scriptsFront-end context confirmedScripts 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:

  1. function_exists() – check before calling any external function
  2. class_exists() – check before instantiating or extending a class from another plugin
  3. 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 init for redirects – redirects at init fire before query parsing, so you cannot check is_page() or is_singular(). Use template_redirect instead.
  • Using wp_head to enqueue scripts – scripts added at wp_head bypass WordPress dependency management. Scripts enqueued at wp_enqueue_scripts are handled correctly with deduplication and dependency resolution.
  • Using init for REST API registration – REST routes added at init work in some cases but the correct hook is rest_api_init. Using the correct hook ensures routes only register when the REST API is actually initializing.
  • Using save_post for calculations that need the post fully saved – save_post fires during the save. If you need data from a related post that is also being updated in the same request, use wp_after_insert_post which 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

MistakeWrongCorrect
PriorityDefault 10, conflicts with othersCheck existing priorities, use specific values
remove_action classnew ClassName()Original instance or global reference
Anonymous functionsadd_action with anonymous functionNamed function or class method
Late bindingHook registered after action firedRegister at the earliest appropriate action
Conditional wrappingis_page() around add_action at initCondition check inside the callback
Filter returnNo return statement in callbackAlways return – original or modified value
init overuseScripts, AJAX, REST all in initwp_enqueue_scripts, wp_ajax_*, rest_api_init
Function exists checkDirect call to external functionfunction_exists() or class_exists() guard
Wrong hookRedirects at init, scripts at wp_headtemplate_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:

  1. Always name your callbacks. Named functions and class methods can always be removed, referenced, and debugged. Anonymous functions cannot.
  2. Always return from filters. Make it a habit – write the return statement before you write the function body.
  3. Check priority before removing. Use has_action() to confirm the registered priority before calling remove_action().
  4. Use the most specific hook for each task. Do not use init because it works. Use the hook that was built for what you are doing.
  5. Guard external dependencies. Wrap every call to an external plugin or uncommon WordPress function in an existence check.
  6. 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.

Visited 1 times, 1 visit(s) today

Last modified: March 6, 2026