Fixing Elementor Pro Soft 404s (and a Fatal) on Non-Existent Tag and Category URLs
If you have an Elementor Pro Theme Builder Archive template, your site may be quietly returning 200 OK for tag and category URLs that don’t exist - /tag/made-up/, /category/typo/, anything a bot or a broken link can invent. WordPress should answer those with a 404. Instead they render an empty archive with a success status: a textbook soft 404. On some sites it’s worse than quiet - if your Archive template uses the Archive URL dynamic tag, the same URLs throw a PHP Fatal error and return a 500. This is a bug in Elementor Pro, not a plugin conflict, and the fix is a small mu-plugin. Here is that first, then why it happens.
Seen on Elementor Pro 4.1.1 with Elementor 4.1.3. It requires an active Theme Builder Archive template; it is not PHP-version specific.
See it on Elementor’s own site
You don’t need a local repro for the soft 404 - Elementor’s own blog does it. Open a category that doesn’t exist:
https://elementor.com/blog/category/elementor-break/
elementor-break is not a real category, yet the page returns 200 and renders the archive shell rather than a 404. That’s the bug in its mildest form.
Read the error
On a site that uses the Archive URL dynamic tag, the mild version becomes a fatal. Here is how it lands in the PHP error log on a request to a non-existent tag:
[18-Jun-2026 10:46:15 UTC] PHP Fatal error: Uncaught Error: Object of class WP_Error could not be converted to string in /path/to/wordpress/wp-content/plugins/elementor/core/dynamic-tags/manager.php:66
Stack trace:
#0 .../elementor/core/dynamic-tags/manager.php(66): preg_replace_callback()
#1 .../elementor/includes/controls/base-data.php(91): Elementor\Base_Data_Control->parse_tags()
#5 .../elementor/core/files/css/post.php(315): Elementor\Controls_Stack->get_parsed_dynamic_settings()
#6 .../elementor/core/files/css/post.php(297): Elementor\Core\Files\CSS\Post->render_element_styles()
#16 .../elementor/core/dynamic-tags/manager.php(517): Elementor\Core\DynamicTags\Manager->after_enqueue_post_css()
#22 .../elementor-pro/modules/theme-builder/classes/locations-manager.php(196): Elementor\Core\Files\CSS\Post->enqueue()
#31 .../wp-content/themes/your-theme/header.php(64): wp_head()
Note where it fires: during wp_head(), while Elementor regenerates the Archive template’s CSS, on a URL whose term doesn’t exist. Both symptoms - the 200 and the 500 - share one root cause a layer up.
Add the mu-plugin
If you’re chasing this down right now, this is the part to act on. Create wp-content/mu-plugins/elementor-missing-term-404-fix.php - the mu-plugins folder may not exist yet, so add it - and drop in the following:
<?php
/**
* Plugin Name: Elementor Pro - Restore 404s on Non-Existent Term Archives
* Description: Forces a proper 404 for tag/category/taxonomy URLs whose term
* doesn't exist, which Elementor Pro's archive pagination filter otherwise
* suppresses (causing soft 404s, and a fatal when the Archive URL tag is used).
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
add_filter( 'pre_handle_404', function ( $handled, $wp_query ) {
// Only term archives: tag, category, or any custom taxonomy.
if ( ! ( $wp_query->is_tag || $wp_query->is_category || $wp_query->is_tax ) ) {
return $handled;
}
// A valid term archive resolves to a WP_Term. Anything else (null / WP_Error)
// means the term doesn't exist, so this should be a 404.
if ( $wp_query->get_queried_object() instanceof WP_Term ) {
return $handled;
}
$wp_query->set_404();
status_header( 404 );
nocache_headers();
return true; // Handled - as a real 404.
}, 99, 2 );
The full file is also on GitHub. It hooks pre_handle_404 at priority 99 - after Elementor’s own callbacks - and only acts when the queried object isn’t a real term, so valid archives and legitimate pagination are untouched. Flush your page cache, reload a made-up tag URL, and it now returns a clean 404. If you use the Archive URL tag, the fatal is gone too, because the request is no longer treated as a term archive by the time the tag is evaluated.
Why it happens
There are two faults stacked on top of each other.
The first is the one that does the damage. Elementor Pro registers a pre_handle_404 filter, Locations_Manager::should_allow_pagination_on_archive_templates(), so that paginated archive templates aren’t wrongly 404’d on page two and beyond. Reasonable enough. But it returns “handled” for any archive that matches the template, with no check that the requested term actually exists:
// elementor-pro/modules/theme-builder/classes/locations-manager.php
public function should_allow_pagination_on_archive_templates( $handled, $wp_query ) {
$is_archive = is_archive() || is_home() || is_search();
if ( $handled || ! $is_archive ) {
return $handled;
}
// ...checks the template's posts widget, never the queried term...
}
And it triggers on page one. should_allow_pagination() returns true even for the first page when the posts widget uses Load More or infinite scroll, or simply has no page limit set:
// elementor-pro/modules/posts/traits/pagination-trait.php
if ( empty( $element['settings']['pagination_page_limit'] ) || $using_ajax_pagination ) {
return true;
}
Returning true from pre_handle_404 short-circuits WordPress’s WP::handle_404() entirely. So set_404() never runs. The status stays 200, and - this is the part that matters for the fatal - is_tag(), is_category() and is_tax() stay true for a term that doesn’t exist.
The second fault only bites once the first has set the stage. With is_tag() still reporting true, the Archive URL dynamic tag asks Elementor for the current archive’s URL:
// elementor-pro/core/utils.php
} elseif ( is_category() || is_tag() || is_tax() ) {
$url = get_term_link( get_queried_object() ); // WP_Error for a missing term
}
// ...
return $url; // returned unguarded
For a non-existent term, get_term_link() returns a WP_Error. That object is handed back as the tag’s value and then cast to a string inside preg_replace_callback() while Elementor builds the element’s CSS - and a WP_Error has no string form, so PHP fatals.
Elementor already knows to guard this. The sibling Internal URL tag does exactly the right thing fifteen files over:
// elementor-pro/modules/dynamic-tags/tags/internal-url.php
if ( ! is_wp_error( $url ) ) {
return $url;
}
return '';
get_the_archive_url() simply doesn’t.
Why most sites only get the silent version
Whether you see a 500 or a soft 404 comes down to one thing: do you use the Archive URL dynamic tag anywhere in the Archive template? Most people don’t, so most sites get the quiet failure - non-existent tag and category URLs returning 200 with an empty archive, day after day, with nothing in the error log. It’s the kind of bug you never notice from a browser, because the page “works”. You find it in Search Console as a pile of soft 404s, or in a crawl report wondering why made-up URLs resolve.
That’s the real cost of the mild case. Every invented or mistyped term URL becomes a thin, indexable 200 instead of an honest 404 - wasted crawl budget, duplicate empty shells, and the occasional one getting indexed. The fatal is louder and easier to spot, but the soft 404 is the one quietly affecting more sites.
The proper fix
Upstream, either fault could be closed and the symptom would change; closing both fixes it properly.
Guard the URL helper, mirroring the Internal URL tag, so the fatal can’t happen:
// get_the_archive_url(), before the final return
if ( is_wp_error( $url ) ) {
$url = '';
}
return $url;
And don’t claim the 404 for an archive whose term doesn’t resolve, so the soft 404 can’t happen either:
// should_allow_pagination_on_archive_templates(), early in the method
if ( ( $wp_query->is_tag || $wp_query->is_category || $wp_query->is_tax )
&& ! ( $wp_query->get_queried_object() instanceof WP_Term ) ) {
return $handled;
}
I’ve reported it to Elementor as issue #36250. Until it ships, the mu-plugin above is safe to leave in permanently - it no-ops on every valid archive and only ever turns a non-existent term into the 404 it always should have been.
And that’s it. Bots can guess all the tag URLs they like; they’ll get a 404, your real archives keep working, and Search Console stops counting soft 404s that were never real pages.