Preventing Fatal Errors on WordPress
Learn about WordPress development best practices and view code examples.
Fatal errors can bring down an entire site, so it’s important to be thorough while developing for a WordPress site on any platform. Let’s discuss some examples that can result in unwanted outages, and how to avoid them.
- Fatal errors: functions colliding
- Fatal errors: functions that don’t exist
- Warnings: not checking return values
- Avoiding post__not_in
Fatal errors: functions colliding
A function might exist in PHP, WordPress core, a plugin, or a theme. But if you declare a function that already exists, PHP will throw a fatal error.
For example, the function below already exists in WordPress and an error will be thrown when declared:
// this will cause a fatal
function get_permalink( $url ) {
// do something
}
The best way to protect against collisions is to wrap their declaration with `function_exists`.
Alternatively, you can use:
- A class method
- A namespace (this tends to introduce complexity)
Fatal errors: functions that don’t exist
PHP also throws a fatal error if you try to access a function that hasn’t been declared, for example, when the function is declared in a plugin but the plugin is not installed or active.
// this will cause a fatal if Safe Redirect Manager is not active
$redirect = srm_create_redirect( '/old/', '/new/', 301);
It’s important to check for this when creating a plugin that relies on another plugin. Because WordPress doesn’t load certain wp-admin files for the front end, it’s worth checking for that function and manually loading the file(s) if necessary.
This can also happen when the declaration has a typo, or by not using a namespace prefix where necessary. These issues are hard to avoid, so “smoke testing” is always useful.
Warnings: not checking return values
Using `WP_DEBUG` in development and displaying warnings and notices is a good development practice. These warning conditions should be immediately obvious and can be quickly addressed and corrected.
PHP warnings and notices won’t necessarily hurt a site, but their presence suggests there’s something that needs attention. Eliminating all warnings and notices is a best practice and makes it easy to spot new issues in the PHP `error_log`.
Note: Leaving too many unnecessary or unaddressed warnings will clog your logs, use disk space, and make it hard to find and address new problems.
Here are common warnings or notices, and example code that causes them.
Accessing an array without making sure it is an array
// expecting an array
$stuff = getStuff( $args );
// unless getStuff is guaranteed to always return an array, this is not good...
foreach ( $stuff as $thing ) { // Invalid argument supplied for foreach()
// actions
}
// this is common and also not good without checking $stuff:
$thing1 = $stuff[0];
// even worse
$thing_name = $stuff[0]->getName();
Using anything that is not a scalar as an index
$redirect_to = [ 'foo' => 'foo.com', 'bar' => 'bar.com' ];
$c = get_categories(); // returns an array
// (other code that fails to convert $c to a single string)
// this will fail because $c is an array
$redirect_url = $redirect_to[ $c ];
Accessing objects that may not be what was expected
// expecting an object
$something = getSomething( $id );
// if $something is null, this is not going to work
$something->doIt();
Certain functions return WP_Error when they fail. It's important to check that condition.
$categories = get_the_terms( $post->ID, 'category' );
// if the result is WP_Error, this next line will throw warnings
foreach ( $categories as $category ) {
$cats[] = $category->name;
}
Avoiding post__not_in
The `WP_Query` argument `post__not_in` may seem like a helpful option, but it can lead to poor performance on a busy and/or large site by affecting the cache hit rate.
The argument `post__not_in` is usually used to exclude specific post IDs from a query’s results. For instance, if a widget shows the five most recent posts on every page, your site designer may want to avoid showing the current post in that widget. That said, this argument is a bit redundant, as the reader is already reading that recent post.
How it’s used
You might have a widget like this:
// Display the most recent news posts
function my_recent_news_widget( ) {
$args = array(
'category_name' => 'news',
'posts_per_page' => 5,
'post_status' => 'publish',
'ignore_sticky_posts' => true,
'no_found_rows' => true,
);
$recent_posts = new WP_Query( $args );
echo '<div class="most-recent-news"><h1>News</h1>';
while ( $recent_posts->have_posts() ) {
$recent_posts->the_post();
the_title( '<h2><a href="' . get_permalink() . '">', '</a></h2>');
}
echo '</div>';
wp_reset_postdata();
}
The typical approach is to modify that function alone, adding an optional function argument and a `post__not_in` query argument on line 9, as follows:
// Display the most recent news posts (but not the current one)
function my_recent_news_widget( $exclude = array() ) {
$args = array(
'category_name' => 'news',
'posts_per_page' => 5,
'post_status' => 'publish',
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'post__not_in' => $exclude,
);
$recent_posts = new WP_Query( $args );
echo '<div class="most-recent-news"><h1>News</h1>';
while ( $recent_posts->have_posts() ) {
$recent_posts->the_post();
the_title( '<h2><a href="' . get_permalink() . '">', '</a></h2>');
}
echo '</div>';
wp_reset_postdata();
}
You’d probably call this in a template with `my_recent_news_widget( [ get_the_ID() ] );` and it would display the five most recent news posts, but not the current post.
While the use above is simple, it’s not ideal.
Problems with this approach
The query, which was previously leveraging the built-in query cache, is now unique for every post or page due to the added `AND ID not in ( ‘12345’ )`. That’s because the cache key (which is a hash of the arguments) now includes a list of at least one ID, which is different across all posts. Instead of subsequent pages obtaining the list of five posts from the object cache, it will miss the cache, and the database will do the same work on multiple pages.
As a result, each of those queries is now cached separately, unnecessarily increasing Memcached use. For a site with hundreds of thousands of posts, this will potentially impact the object cache size and result in premature cache evictions.
What to do instead
In almost all cases, you can significantly improve speed by requesting more posts and skipping the excluded posts in PHP.
Improve performance by ensuring the post query is consistent across all the URLs, retrieving the six most recent posts, so the posts are surfaced from the object cache. If you anticipate the `$exclude` list to be larger than one post, set the limit higher, perhaps to 10. Make it a fixed number, not a variable, to reduce the number of cache variants.
The updated function no longer excludes the post(s) in SQL, it uses conditionals in the loop:
// Display the most recent news posts (but not the current one)
function my_recent_news_widget( $exclude = array() ) {
$args = array(
'category_name' => 'news',
'posts_per_page' => 10,
'post_status' => 'publish',
'ignore_sticky_posts' => true,
'no_found_rows' => true,
);
$recent_posts = new WP_Query( $args );
echo '<div class="most-recent-news"><h1>News</h1>';
$posts = 0; // count the posts displayed, up to 5
while ( $recent_posts->have_posts() && $posts < 5 ) {
$recent_posts->the_post();
$current = get_the_ID();
if ( ! in_array( $current, $exclude ) ) {
$posts++;
the_title( '<h2><a href="' . get_permalink() . '">', '</a></h2>');
}
}
echo '</div>';
wp_reset_postdata();
}
While requiring a bit of logic in PHP, this approach leverages the query cache better and avoids creating cache variations that might impact the site’s scalability and stability.
Learn more about WordPress best practices
For even more technical advice on developing for WordPress and WordPress VIP, review our documentation.