TechEarl

Fix 'Call to undefined function get_option()' in WordPress

How to fix the 'Call to undefined function get_option()' fatal error in WordPress. Covers premature WP function calls, the wp-load.php bootstrap, the modern WP-CLI alternative, and the plugin-developer checklist.

Ishan Karunaratne⏱️ 19 min readUpdated
Share thisCopied
How to fix the 'Call to undefined function get_option()' fatal error in WordPress. Covers premature WP function calls, the wp-load.php bootstrap, the modern WP-CLI alternative, and the plugin-developer checklist.

Fatal error: Call to undefined function get_option() almost never means WordPress is broken. It means PHP reached a call to get_option() (or wp_insert_post(), or current_user_can(), or any other WordPress function) before wp-load.php had a chance to define it. The fix is to make sure WordPress is loaded before your code runs, or to restructure the code so it runs from inside a WordPress request lifecycle.

How do I fix 'Call to undefined function get_option()' in WordPress?

The error is raised when PHP encounters get_option() but the WordPress runtime isn't loaded. get_option() only becomes available once WordPress runs its bootstrap: wp-load.php loads wp-config.php, which loads wp-settings.php, which (via wp-includes/functions.php) pulls in wp-includes/option.php, where get_option() is defined. If a standalone PHP file or a cron script calls WordPress functions without bootstrapping WordPress first, the function doesn't exist yet and PHP throws a fatal.

The fast fix for standalone scripts that genuinely need WordPress functions: bootstrap WordPress by requiring wp-load.php before your code runs. The better fix for plugin and theme code: never call WordPress functions at file-load time. Wrap everything in a callback hooked to init, plugins_loaded, or whichever WordPress hook fires after the runtime is ready.

Jump to:

Root cause: when get_option() doesn't exist yet

The WordPress function load order is, simplified:

WordPress bootstrap load order: the include chain from index.php through wp-settings.php, where plugin.php defines do_action() early and option.php (loaded via functions.php) defines get_option(), then the plugins_loaded and init actions fire before the request proceeds
WordPress function load order. plugin.php (do_action) loads early; get_option() is defined later when wp-settings.php pulls in option.php via functions.php. Call get_option() outside this bootstrap and PHP throws the fatal.

wp-settings.php references on the order of 300 core files, not the "500+" sometimes quoted, and option.php is reached indirectly: wp-settings.php requires wp-includes/functions.php, which in turn requires wp-includes/option.php. The practical point is unchanged: nothing you write can call get_option() until that bootstrap has run.

Any code that calls get_option() before that chain has completed gets the fatal error. The original 2015 framing of this error as "corrupted core files" is misleading in the vast majority of cases. Corrupted core is one possible cause, but a far rarer one than premature function calls.

The most common scenarios

All of these share one root: code calls get_option() before WordPress has bootstrapped far enough to define it. The six places I actually see it happen in 2026:

1. A standalone PHP file accessed directly

A developer drops a file like /wp-content/themes/mytheme/scripts/import.php and hits it from the browser at https://example.com/wp-content/themes/mytheme/scripts/import.php. PHP serves the file directly, wp-load.php is never executed for that URL. The first get_option() call inside the file throws.

2. Custom cron script run from system cron

bash
# /etc/cron.d/site-cron
0 * * * * www-data php /var/www/html/wp-content/scripts/cleanup.php

Same problem: cleanup.php was invoked directly. PHP didn't go through Apache or Nginx, so the WordPress request lifecycle never fired.

3. Plugin code at file-level scope

php
// Bad: runs at require() time, before init has fired
$option = get_option( 'my_plugin_settings' );

Be precise about what actually breaks here. A normally activated plugin is loaded near the end of wp-settings.php, long after option.php (pulled in via functions.php early in the bootstrap), so get_option() is already defined and the database is connected. A regular plugin will not throw the undefined-function fatal from this. The cases that genuinely do are narrower:

  • The plugin file is requested directly by URL (the same root cause as scenario 1), for example a custom endpoint that reads php://input and handles its own POST. No bootstrap ran, so get_option() really is undefined.
  • A very early drop-in, specifically advanced-cache.php, loads near the top of wp-settings.php (before functions.php pulls in option.php), so it genuinely cannot call get_option(). This is unique to advanced-cache.php: db.php, object-cache.php, must-use plugins, and normal active plugins all load after option.php, so get_option() is already defined for them.

Watch for the silent version. When get_option() is undefined but display_errors is off (the production default), the fatal halts the script with no visible message, so the symptom looks like the call "returning" an empty value or the endpoint returning a blank or 500 response, not a printed Call to undefined function. Check the PHP error log (or set WP_DEBUG) and the real error is there. And even when get_option() is defined, calling it at file-load is still fragile: it runs before other plugins have registered their pre_option_* / option_* filters, so a value you capture in a constant at that point can be stale or unfilterable.

4. Composer autoload conflicts

A plugin uses Composer's autoloader and includes a class that calls get_option() in its constructor. If the class is instantiated during composer.json post-install hooks (or anywhere outside a normal WordPress request), get_option isn't loaded.

5. wp-config.php calls a WordPress function

Putting a get_option() call (or any WordPress function) into wp-config.php above its closing require_once ABSPATH . 'wp-settings.php'; line runs it before WordPress exists, so it throws. wp-config.php is configuration that loads before the runtime: nothing in it can use the WordPress API. Keep it to plain PHP constants, and if you need option-driven behaviour, move that logic into a must-use plugin (wp-content/mu-plugins/), which loads after the bootstrap and can call get_option() safely.

6. Truly corrupted core files

Rare in 2026, but if you can confirm wp-includes/option.php is missing or contains a parse error, then yes, a core file replacement is the fix. This was much more common in shared-hosting eras when files genuinely got corrupted during FTP uploads. (A stray BOM or whitespace after a closing ?> in wp-config.php can break the bootstrap the same way.)

The fast fix: bootstrap with wp-load.php

For a standalone script that legitimately needs WordPress functions, require wp-load.php at the top:

php
<?php
// /var/www/html/wp-content/scripts/cleanup.php

require_once dirname( __FILE__ ) . '/../../../wp-load.php';

// Now WordPress is loaded
$option = get_option( 'siteurl' );
echo $option;

The path navigation dirname( __FILE__ ) . '/../../../wp-load.php' walks three directories up from wp-content/scripts/ to the WordPress root. Adjust the count based on where your script actually lives.

If that path is wrong, the require fails and you get this exact fatal anyway (the function is still never defined). For a script whose location can move, use an absolute path or walk up the tree to locate wp-load.php. And know that some installs keep it in a non-standard place: Debian and Bitnami packages, and Bedrock/roots setups (where core lives under web/wp/, so it is web/wp/wp-load.php), all change the hop count.

This works but has costs:

  • It loads the entire WordPress stack, every plugin, every theme function, all the database connections. For a five-line cleanup task, that's a lot of overhead.
  • Errors in any active plugin during bootstrap will kill your script.
  • You inherit the request's user context (or none, for CLI), which can lead to permission-related surprises.

For one-off cleanup scripts it's fine. For anything you'll run regularly, prefer WP-CLI.

Load less of WordPress with SHORTINIT

SHORTINIT is a WordPress constant that, defined as true before wp-load.php is required, halts the bootstrap partway through wp-settings.php (at the SHORTINIT check, before plugins and themes load) so only a minimal core subset is loaded. If your script only touches the database and the options API, you don't need the whole stack just to read an option:

php
<?php
// Only $wpdb, the object cache, the hooks API and the options API load.
// No plugins, no theme, no REST API.
define( 'SHORTINIT', true );
require_once dirname( __FILE__ ) . '/../../../wp-load.php';

$siteurl = get_option( 'siteurl' );   // works: option.php loads before the SHORTINIT cut-off
global $wpdb;
$rows = $wpdb->get_col( "SELECT option_name FROM {$wpdb->options} LIMIT 5" );

SHORTINIT aborts wp-settings.php right after it has set up $wpdb, the object cache, the hooks API (do_action / add_filter), and the options API (get_option / update_option). What it deliberately does not load:

  • Plugins (plugins_loaded and init never fire), so any pre_option_* / option_* filters a plugin would normally add are not registered. get_option() still runs core's own default option filters (the ones registered before the cut-off, for example on home, siteurl, blog_charset), so the value is not guaranteed to be the literal database value, it just skips plugin-added filtering. For a maintenance script that is usually what you want.
  • Themes and template tags.
  • Users, roles, and capabilities, so current_user_can(), wp_get_current_user(), and the auth/pluggable functions (wp_mail(), cookie auth) are unavailable.
  • Taxonomies, post types, WP_Query, rewrite rules, localisation, and the REST API.

Use it for fast, low-overhead scripts that read or write options and tables directly. Go back to the full bootstrap (or WP-CLI) the moment you need a plugin's behaviour, a capability check, or anything theme-, query-, or REST-related.

The modern preferred approach: WP-CLI

WP-CLI handles bootstrapping for you and gives you a per-command API:

bash
# Read a single option
wp option get siteurl

# List all options matching a pattern
wp option list --search='my_plugin_*' --format=table

# Update an option
wp option update my_plugin_settings '{"key":"value"}' --format=json

# Run arbitrary PHP with WordPress loaded
wp eval 'echo get_option("siteurl");'

# Run a PHP file with WordPress loaded
wp eval-file path/to/script.php

wp eval-file is the modern replacement for the "require wp-load.php" pattern. WP-CLI loads WordPress once and runs your file in that context. From cron:

bash
0 * * * * www-data /usr/local/bin/wp --path=/var/www/html eval-file /var/www/html/wp-content/scripts/cleanup.php --quiet

The --path flag tells WP-CLI where the WordPress install lives. --quiet suppresses non-error output (cron will email anything that hits stdout otherwise).

For one-off interactive debugging, wp shell drops you into a REPL with WordPress loaded, far better than dropping var_dump() calls into a theme file.

Run it over HTTP: REST API or admin-ajax

If the reason you reached for a standalone file is that something has to call WordPress over HTTP (a webhook, an AJAX action, a small API the front end posts to), do not drop a raw PHP file in wp-content/ and require wp-load.php. That is exactly the pattern that throws this error when the file is hit directly, and it ships with zero access control. Register a real endpoint instead, where WordPress is already bootstrapped and you decide who may call it.

REST API (preferred):

php
add_action( 'rest_api_init', function () {
    register_rest_route( 'myplugin/v1', '/cleanup', [
        'methods'  => 'POST',
        'callback' => 'te_cleanup_handler',
        // REQUIRED. This is the access gate, not an afterthought. Never use
        // '__return_true' on an endpoint that changes data or returns anything
        // sensitive: that publishes it to the entire internet.
        'permission_callback' => function () {
            return current_user_can( 'manage_options' );
        },
    ] );
} );

function te_cleanup_handler( WP_REST_Request $request ) {
    // WordPress is fully loaded here, so get_option() always exists.
    $value = get_option( 'my_plugin_settings' );
    return new WP_REST_Response( [ 'ok' => true, 'value' => $value ], 200 );
}

The route lives at /wp-json/myplugin/v1/cleanup, WordPress is always loaded for it, so the fatal cannot happen. Since WordPress 5.5, every route should declare permission_callback: omit it and core logs a _doing_it_wrong notice (the route still registers, it is not a hard fatal). A genuinely public route should set it to __return_true explicitly; a privileged or data-changing route must do a real authorization check, such as current_user_can( 'manage_options' ). The permission_callback is the authorization gate. Authentication is separate: a browser making a cookie-authenticated call sends an X-WP-Nonce header generated with wp_create_nonce( 'wp_rest' ), which core's REST cookie auth verifies for CSRF protection; external clients and webhooks should use Application Passwords or another real auth method, not a nonce.

admin-ajax (the older mechanism, still common):

php
// Privileged action: register the logged-in hook only.
add_action( 'wp_ajax_te_cleanup', 'te_cleanup_handler' );
// Add wp_ajax_nopriv_te_cleanup ONLY for a genuinely public action. Registering
// it for an admin-only action just exposes a path that always fails the check.

function te_cleanup_handler() {
    check_ajax_referer( 'te_cleanup' );                 // nonce check
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_send_json_error( 'forbidden', 403 );
    }
    wp_send_json_success( [ 'value' => get_option( 'my_plugin_settings' ) ] );
}

Called at /wp-admin/admin-ajax.php?action=te_cleanup. Same principle: WordPress bootstraps admin-ajax.php for you, and the nonce plus capability check is the access control you have to write yourself. REST is the modern choice; admin-ajax is fine for code that already uses it. Either way the lesson is the same as the rest of this article: let WordPress load your code inside a real request, rather than loading WordPress from your code.

Hook into init instead of file-level scope

Inside a plugin or theme, never call WordPress functions at the top level of the file. The right pattern:

php
<?php
/**
 * Plugin Name: My Settings Reader
 */

// BAD: runs before WordPress may be ready
// $option = get_option( 'my_plugin_settings' );

// GOOD: defer until init
function my_plugin_load_settings() {
    $option = get_option( 'my_plugin_settings' );
    // ... use $option
}
add_action( 'init', 'my_plugin_load_settings' );

init fires after WordPress core, all active plugins, and the active theme's functions.php have finished loading. At that point the normal front-end APIs are available (get_option(), hooks, post types, taxonomies, WP_Query, capability checks), the database is connected, and the current user is resolved. (A handful of admin-only includes load only inside /wp-admin/ requests, so don't assume literally every function exists on the front end, but anything in this article's scope is ready.)

For code that needs WordPress functions but must run before init (rare), use plugins_loaded, fires once all plugins are loaded but before init:

php
function my_plugin_early_setup() {
    // get_option is available here
    if ( get_option( 'my_plugin_feature_flag' ) ) {
        // register early hooks
    }
}
add_action( 'plugins_loaded', 'my_plugin_early_setup' );

Plugin developer checklist

When writing a plugin, the rules that prevent this error:

  1. Don't do WordPress-dependent work at file scope. In a normally activated plugin get_option() is already defined at load time, so this rarely throws the undefined-function fatal, but reading and caching an option there runs before other plugins, the current user, and request context are ready, so the value can be stale, wrong for the context, or unfilterable. Put real work inside hooked callbacks.
  2. No database queries before plugins_loaded. $wpdb is available earlier but using it before plugins are loaded can race with other plugins' table-creation hooks.
  3. Use register_activation_hook() for one-time setup. Don't run install logic at file load.
  4. Guard against direct file access. Add if ( ! defined( 'ABSPATH' ) ) { exit; } at the top of every PHP file in your plugin.
  5. Don't include files that include WordPress. A plugin file that does require 'wp-load.php' creates an infinite-loop scenario when WordPress loads the plugin.

The ABSPATH guard is the canonical defense against the entire class of "scripts being accessed directly" bugs:

php
<?php
/**
 * Plugin Name: My Plugin
 */

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Prevent direct file access
}

// ... rest of plugin

If a curious visitor hits https://example.com/wp-content/plugins/my-plugin/my-plugin.php directly, PHP would otherwise execute the file with no WordPress context. The ABSPATH check turns that into a clean exit.

PHP 8.x vs PHP 7.x error format

The error message text shifted slightly between PHP versions:

PHP versionError text
PHP 7.xFatal error: Call to undefined function get_option() in /path/to/file.php on line 12
PHP 8.0+Fatal error: Uncaught Error: Call to undefined function get_option() in /path/to/file.php:12

PHP 8 wraps the fatal in an Error exception that includes a stack trace, which is significantly more useful for debugging. If you're still on PHP 7.x and seeing the older format, enabling display_errors and error_reporting=E_ALL in your wp-config.php will at least show the file and line number:

php
// wp-config.php (development only)
define( 'WP_DEBUG', true );
define( 'WP_DEBUG_LOG', true );
define( 'WP_DEBUG_DISPLAY', false );
@ini_set( 'display_errors', 0 );

Errors go to wp-content/debug.log rather than the browser, which is safer in production-adjacent environments.

If the error is hitting your script before WordPress is loaded enough to set those constants, fall back to native PHP error logging:

php
ini_set( 'log_errors', '1' );
ini_set( 'error_log', '/var/log/php-errors.log' );

Troubleshooting matrix

SymptomLikely causeFix
Error in a standalone scriptScript doesn't bootstrap WordPressAdd require_once '/path/to/wp-load.php'; or run via WP-CLI
Error during plugin activationActivation file run directly or included outside WordPress, or a different undefined function at file scope (get_option is already defined during normal activation)Activate via wp-admin, add the ABSPATH guard, move install logic into register_activation_hook()
Error in cron jobScript invoked directly by cronSwitch cron to call wp eval-file via WP-CLI
Error only on multisiteCode bypasses the WordPress bootstrap, or runs in an early drop-in context (is_multisite() itself loads very early, in wp-includes/load.php)Run the code inside WordPress or bootstrap it correctly; don't defer is_multisite() just because plugins_loaded hasn't fired
Error pointing at a wp-config.php lineA WordPress function called in wp-config.php before the wp-settings.php requireKeep wp-config.php to constants; move WP API calls into a must-use plugin
Error after composer installClass instantiated outside WordPress lifecycleMove construction to a hook callback
Error after PHP upgradePlugin used a function deprecated in newer PHPCheck error_log for "deprecated" notices; update plugin
Error after WordPress core updateGenuinely corrupted core filesRe-download matching WordPress version, overwrite wp-admin and wp-includes
Error only at admin URLThe undefined function is admin-only or pluggable (not get_option, which loads earlier), or a required admin include hasn't loadedMove admin-only work to admin_init, and load needed wp-admin/includes files explicitly
ABSPATH not definedDirect file access bypassing index.phpAdd if ( ! defined( 'ABSPATH' ) ) { exit; } and re-route via index.php

For specific WordPress-related PHP issues:

What to do next

For related WordPress troubleshooting:

FAQ

PHP reached a call to get_option() before the function was defined. WordPress defines get_option() in wp-includes/option.php, which wp-settings.php pulls in via wp-includes/functions.php, and wp-settings.php is loaded by wp-load.php. If that bootstrap hasn't run yet, the function doesn't exist and PHP raises a fatal error.

The same error pattern applies to every WordPress function, wp_insert_post, current_user_can, do_action. The error is always "WordPress wasn't loaded when this code ran".

It works for one-off scripts, but it's not the modern preferred approach. wp-load.php bootstraps the entire WordPress stack, every plugin, theme, database connection, which is heavy for a five-line cleanup task.

For anything you'll run repeatedly, use WP-CLI: wp eval-file path/to/script.php. WP-CLI bootstraps WordPress once and gives you a stable command-line API. For ad hoc one-shots, the wp-load.php require is acceptable.

For a normally activated plugin it usually is not a fatal: active plugins load near the end of wp-settings.php, after option.php is pulled in via functions.php, so get_option() is defined and the database is connected. The reason it is still a bug is timing and filterability. File-level code runs on every request before other plugins have registered their pre_option_* / option_* filters and before the user, query, and locale are known, so a value you cache in a constant at load time can be stale, unfilterable, or wrong for the context.

The genuine fatals come from a different place: a plugin file accessed directly by URL with no bootstrap, or a drop-in (advanced-cache.php, db.php) that loads before option.php. In production with display_errors off, that fatal is silent and looks like an empty value, so check the error log.

Either way, the fix is to wrap the call in a function and hook it to init or plugins_loaded.

SHORTINIT is a WordPress constant. Define it as true before requiring wp-load.php and WordPress halts its bootstrap partway through wp-settings.php, loading only a minimal core subset: $wpdb, the object cache, the hooks API, and the options API. Plugins, themes, the REST API, and the users/capabilities layer (current_user_can()) are not loaded.

Use it for fast maintenance scripts that only read or write options or query tables directly, where booting the entire stack would be wasteful. Reach for the full bootstrap or WP-CLI the moment you need a plugin's behaviour, a capability check, or anything theme-, query-, or REST-related.

plugins_loaded fires after all active plugins have been loaded but before the theme and before init. Use it when you need to register early hooks or check feature flags that other plugins might also need.

init fires after the theme's functions.php has loaded and the request is fully bootstrapped. Use it as the default, 95% of plugin setup belongs here.

For admin-only code, admin_init fires only inside /wp-admin/ requests, which is more efficient than a global init handler.

WP-CLI is a CLI binary that knows how to bootstrap WordPress correctly. It reads wp-config.php, loads wp-load.php with the right context, sets up the request lifecycle artificially (no HTTP request actually happens), and then runs your command.

From your script's perspective, every WordPress function is available from the first line. wp eval-file your-script.php is the cleanest way to run WordPress-dependent PHP without thinking about bootstrap order.

Possibly, but rarely in 2026. Corrupted core files were a common cause when most WordPress installs were managed via FTP on shared hosting, partial uploads happened often. Modern hosting deploys via Git, Composer, or managed CI, so core files arrive intact.

Before assuming corruption, check whether the error is in your code (standalone script, plugin, cron). If it definitely traces to a core file, re-download the same WordPress version from wordpress.org/download/releases/ and overwrite wp-admin and wp-includes, preserving wp-content and wp-config.php.

PHP 8 changed how undefined-function calls are reported. PHP 7 emits Fatal error: Call to undefined function get_option() in /path/to/file.php on line 12. PHP 8 wraps it in an Error exception: Fatal error: Uncaught Error: Call to undefined function get_option() in /path/to/file.php:12 with a stack trace.

The stack trace in PHP 8 is significantly more useful for tracing which file caused the bootstrap order issue. If you're debugging on PHP 7, upgrade to 8.x first; the debugging experience alone justifies it.

Direct file access. If a visitor hits https://example.com/wp-content/plugins/my-plugin/my-plugin.php in their browser, PHP executes that file standalone with no WordPress context, no get_option, no database. Adding an ABSPATH guard at the top of every plugin PHP file makes those direct hits return an empty response instead of crashing or leaking source.

It's defense-in-depth: your web server config should also block direct access to /wp-content/plugins/ for non-asset files, but ABSPATH is the in-PHP backstop.

See also

TagsWordPressErrorsDebuggingPHPTroubleshootingwp-load.phpWP-CLI

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

How to catch and route your own 404s in WordPress: hook template_redirect, check is_404(), parse the requested path, then resolve to content with status_header(200), 301-redirect with wp_safe_redirect(), or let it fall through to the real 404.

Catch and Route Your Own 404s in WordPress

Intercept requests that would 404 in WordPress on template_redirect, then resolve them to real content with status_header(200), 301 to the right URL with wp_safe_redirect(), or let them fall through. A fallback router for dynamic and legacy slugs you cannot enumerate.

Comparing Advanced Custom Fields with native WordPress post meta, and how both store data in the same wp_postmeta table

ACF Fields vs Native Post Meta in WordPress

ACF and native post meta both write to the same wp_postmeta table. Here is what register_post_meta gives you, what ACF adds on top, and the read/write rules so a bulk script and a content editor never fight over the same field.