The init Hook Is Too Late: Choosing the Right Hook for the Job

There are hundreds of hooks and you need one. Choose by what must already exist when your code runs, not by what the last tutorial happened to use.

WordPress hook timeline comparing plugins_loaded, after_setup_theme, init, wp, and template_redirect

Nobody memorizes the hook list. What experienced WordPress developers actually carry is a much smaller thing: a habit of asking one question before they write add_action.

What must already exist for this code to be correct?

Answer that and the hook chooses itself, because each stage of a request is defined by what has finished by the time it fires. Skip it, and you get init by default, because init is what the tutorials use.

init is a decent default. It is also wrong in two opposite directions often enough to be worth naming.

The question, applied

Write down what your code needs. Then take the latest thing on that list and hook after it.

Your code needsSo it must run afterReasonable hook
Nothing but WordPress functionsPlugins are loadedplugins_loaded
A function defined in the themeThe theme is loadedafter_setup_theme
To declare theme supportBefore theme features are readafter_setup_theme
A custom post type to be registeredNothing specialinit
A taxonomy attached to that post typeThe post type existsinit, later priority
Everything loaded, nothing about this URLAll of the abovewp_loaded
To know which page this isThe main query has runwp
To redirect before outputThe query, before headerstemplate_redirect
To load a stylesheet on one page onlyThe querywp_enqueue_scripts
To append to post outputThe loopthe_content (a filter)
Admin screens onlyThe admin is bootstrappedadmin_init

The full stage-by-stage timeline behind that table, including what is loaded at each point and why the theme loads after the plugins, is set out in the WordPress execution order. This page is the decision procedure that sits on top of it.

Too late: theme features

Some declarations are read at a fixed moment, and hooking after that moment means your declaration is never seen.

// Wrong. init runs after the theme has been set up.
add_action( 'init', function () {
    add_theme_support( 'post-thumbnails' );
} );
// Right.
add_action( 'after_setup_theme', function () {
    add_theme_support( 'post-thumbnails' );
} );

Nothing errors. The featured image box just is not there, and you go looking for a broken template.

The general shape: anything that registers a capability of the theme belongs on after_setup_theme. Theme support flags, image sizes tied to theme support, navigation menu locations, text domain loading in a theme.

Too early: anything about this request

The opposite mistake is the more common one, and it fails silently rather than visibly.

add_action( 'init', function () {
    if ( is_single() ) {                   // always false
        add_filter( 'the_content', 'my_addition' );
    }
} );

On init, the URL has not been parsed into a query yet. There is no “current post”. is_single(), is_page(), is_home(), is_archive(), is_404() and get_queried_object() all have nothing to report on and return the empty answer, on every request.

The earliest reliable point is wp. For anything that produces output, use the hook that belongs to that output: template_redirect for a redirect, wp_enqueue_scripts for assets, the relevant content filter for content.

This is a silent failure, which is what makes it expensive. There is no notice, no log line, and the code reads correctly. It is worth recognizing the category rather than the individual instance.

The two-hook pattern

When you need both a decision about this request and a registration that must happen early, use two hooks and let the later one do the deciding:

add_action( 'init', function () {
    register_post_type( 'guide', $args );     // must be init
} );

add_action( 'wp', function () {
    if ( is_singular( 'guide' ) ) {
        add_filter( 'the_content', 'my_guide_footer' );
    }
} );

Registration is unconditional and early. The conditional attachment is late. Trying to do both at once is what produces the always-false branch above.

Admin, front end, and the hooks that only fire on one

init fires on both. admin_init fires only in the admin, and only after the admin has begun bootstrapping. wp_enqueue_scripts is the front end; admin_enqueue_scripts is the admin; login_enqueue_scripts is the login screen. They are different hooks, not variants.

Two things that catch people:

  • Ajax and REST requests are not “the admin” for your purposes, even when Ajax runs through the admin entry point. Code that assumes a screen exists will misbehave there.
  • is_admin() is not a permission check. It answers “is this an admin-side request”, not “is this person allowed”. Capability checks are a separate subject and a separate call.

When you cannot find one

Sometimes the hook you want does not exist, and the honest answer is that the extension point is missing. Before you conclude that, do the search properly: look for do_action and apply_filters in the file you are trying to influence. That is a real search against real source, and it is the only way to be sure.

It is also the step to take when a hook name arrives from any secondhand source, a tutorial, a snippet site, a generated suggestion, and looks plausible. Hook names are exactly the kind of detail that is easy to produce and easy to get slightly wrong, and a name that does not exist registers happily and never fires. Checking before you build on it is the same discipline as confirming a function that was invented and does not exist, applied to hooks.

If the hook truly is not there, your options are to work from a nearby hook and correct the result, or to file the request upstream. Neither is elegant. Both beat hooking something that never fires.

The short version

  • Ask what must already exist. Hook after the latest item on that list.
  • Theme capabilities go on after_setup_theme. init is too late for them.
  • Nothing about the current page is known on init. wp is the earliest reliable point.
  • Register early and unconditionally; decide late and conditionally.
  • Verify a hook name against a real do_action or apply_filters before you build on it.
Written by

Shah Alom

Leave a Reply

Your email address will not be published. Required fields are marked *