The 2 AM Warning: Why Your Site is Screaming
Few things kill the mood of a successful deployment faster than a PHP warning splashed across your site’s header. It usually happens at the worst time—right after a client launch or during a routine update. While a warning won't necessarily take your site offline like a 500 Fatal Error, it looks messy, leaks your server's file structure, and signals that your code isn't handling data correctly.
The culprit usually looks like this in your debug.log:
Warning: Invalid argument supplied for foreach() in /var/www/html/wp-content/themes/mytheme/functions.php on line 125
PHP is literal. When you use foreach, you are telling the server to step through a list. If that list turns out to be a single number, a "false" boolean, or just empty space (null), PHP stops and complains. In the WordPress ecosystem, this occurs because functions often return false or WP_Error when they can't find what you asked for.
The Breakdown: Why the Loop Fails
A foreach loop requires an array or an object. Anything else triggers the warning. In a typical WordPress build, you'll run into this in four specific places:
- ACF Fields: Using
get_field()on a Repeater or Gallery that hasn't been populated yet. - Custom Database Work: Running
$wpdb->get_results()and receiving an error object instead of a row set. - REST API Integrations: Fetching data from an external service that returns a 404 or a timeout.
- Metadata: Fetching
get_post_metawith the$singleparameter set to true, which returns a string instead of the expected array.
Surgical Fixes and Defensive Strategies
Approach 1: The Quick "Duct Tape" Fix
Need the warning gone immediately? Type casting is your fastest route. By forcing the variable into an array format, you ensure the loop always receives a valid list—even if that list is empty.
// Line 125 in functions.php
$items = get_post_meta( get_the_ID(), 'my_custom_list', true );
// Force the variable to behave like an array
foreach ( (array) $items as $item ) {
echo esc_html( $item );
}
The Trade-off: This is a great temporary bandage for production. However, it hides the symptom without fixing the cause. If $items is empty because of a broken database connection, you'll never see the error that helps you debug it.
Approach 2: The "Safety Gate" (Recommended)
Modern PHP development relies on defensive programming. Don't assume your data exists. Instead, verify it before you try to use it. Since PHP 7.1, we can use is_iterable() to check for both arrays and objects in one go.
$gallery_images = get_field('property_gallery'); // Common ACF Gallery
if ( is_iterable( $gallery_images ) ) {
foreach ( $gallery_images as $image ) {
echo '<img src="' . esc_url($image['url']) . '" alt="" />';
}
} else {
// Log the missing data or provide a fallback UI
error_log('Missing gallery for Post ID: ' . get_the_ID());
}
Approach 3: Establishing Defaults
Clean code often means ensuring a function always returns the same data type. If a function is supposed to return a list of team members, make sure it returns an empty array—not null—if no members are found.
function get_custom_team_members() {
$members = get_option('my_team_list');
// Return early if the data is corrupted or missing
if ( ! is_array( $members ) ) {
return [];
}
return $members;
}
// The loop below is now 100% safe
$team = get_custom_team_members();
foreach ( $team as $member ) {
// Code here runs only if members exist
}
Context Matters: ACF and $wpdb
Advanced Custom Fields is the #1 source of this error. When a user creates a Repeater field but leaves it empty, ACF returns false. If your template assumes that field is an array, your site breaks. Always wrap ACF loops in an if( have_rows() ) or is_array() check.
For custom SQL, $wpdb queries are vulnerable too. If your SQL syntax is slightly off, $wpdb->get_results() might return null. Always verify that !empty($results) before iterating.
Verification: Proving the Fix Works
- Force an Empty State: Go to the WordPress editor and delete the data in the problematic field. The page should load cleanly with no warnings.
- Watch the Logs: Open your terminal and run
tail -f wp-content/debug.log. Refresh your site 3-4 times to ensure no new entries pop up. - Check PHP 8 Strictness: If you recently upgraded to PHP 8.0 or 8.1, remember that the engine is less forgiving. These defensive checks aren't just "good practice" anymore; they are required for stability.
Final Thoughts
Stop writing code that hopes for the best. External APIs fail, users skip fields, and databases hiccup. By using is_iterable() or type casting, you build a theme that stays silent and professional, even when data goes missing.

