Actions and filters both use the same underlying hook system, which is exactly why they get confused. The distinction is simple once it clicks: actions do something at a point in time, filters modify data and hand it back.
Actions: fire and forget
add_action('user_register', function($user_id) {
wp_mail(get_userdata($user_id)->user_email, 'Welcome', 'Thanks for registering.');
});
An action callback doesn’t return anything WordPress uses. It runs, does its job (send an email, log something, update a related record), and that’s it.
Filters: always return something
add_filter('the_title', function($title, $post_id) {
if (get_post_type($post_id) === 'product' && !empty(get_field('is_sale', $post_id))) {
return $title . ' (Sale)';
}
return $title;
}, 10, 2);
The most common bug with filters: forgetting to return the original value when your condition doesn’t apply. Miss that return $title; at the end and every title on the site silently becomes empty — a genuinely common cause of “my whole site’s titles disappeared” bug reports.
Priority actually matters
add_action('init', 'first_function', 5); // runs earlier
add_action('init', 'second_function', 20); // runs later
Default priority is 10. If two plugins hook the same action and one depends on the other having run first, priority is how you control that order — lower numbers run earlier. This is the usual fix when “plugin A doesn’t work unless plugin B loads first.”
Removing a hook someone else added
remove_action('wp_head', 'wp_generator'); // hides the WP version number in page source
To remove a hook, the priority and argument count in your remove_action call have to match exactly what was used in the original add_action call — if the original used priority 20 and you call remove_action with the default (10), nothing happens and no error tells you why.