Pagination Not Working for Custom Taxonomy with Custom Query – 404 Error

In short, your code is fine, but the custom query’s pagination and the one for the main query, they can’t be using the same paged URL query string (which WordPress reads the value from for the paged query arg used in a WP_Query request).

Why the error 404?

So for examples, paginated/paged category requests can have one of these URLs:

  • Full pretty URL:
    https://example.com/category/uncategorized/page/2/

  • Semi-pretty URL which redirects to the above URL:
    https://example.com/category/uncategorized/?paged=2

  • “Ugly URL” (e.g. when permalinks are not enabled)
    https://example.com/?cat=1&paged=2

And as you can see, the URLs are using the query string paged which you can get the value using get_query_var( 'paged' ). (Note that in the first URL, the page/2 is equivalent to paged=2)

And because a category/taxonomy archive is a non-singular request (just like search results pages), the paged value will be used in the main query’s request, i.e. the SQL command for querying the requested archive, unless of course if pagination is not enabled for the main query.

Therefore, the paged value needs to be valid, i.e. it must not exceed the max number of pages for that specific request (the main query). And if the max is exceeded, then you’d get a 404 error because there were no more posts found via the main query. Just like a book with 10 pages (including covers); there’s logically no page #11, right?

So because your custom query is being paginated using the paged query string, then if you get the error 404, it’s likely because the custom query had more pages than the main query.

Sample scenario demonstrating the above:

  • On a category archive page (e.g. at example.com/category/uncategorized), there were 3 pages shown in the pagination. And that’s for the main query which WordPress runs first on page load.

  • Then in the category template, you run a custom query for, maybe a custom post type.

  • Then you added a pagination for the custom query and there were 5 pages shown in that pagination.

  • So if both the paginations are using the same paged query string, then for example, going to page #4 will cause a 404 error because the main query (for the category) had only at most 3 pages of results.

How to fix the error

(Note: These are listed in no specific order.)

  • As I said in the comments, you can use a custom URL query string like page_num, pg, etc. along with paged.

    So the paged will always be just for the main query, while page_num or pg will be used with your custom query.

  • Use AJAX instead to paginate the custom query, but AJAX is not in scope of this answer. (Or that it’s up to you to look for a solution and implement it.)

  • Use a static Page (post of the page type), e.g. at example.com/my-tax-archive, and run your custom queries in the Page Template.

    For singular requests, WordPress doesn’t use the paged in the request SQL, so the paged value can be any number (2, 20, 200, 2000, etc.).

But there’s a trick to make paged works for both the main query and custom queries.. on archive pages.

  1. Add this to your theme’s functions.php file:

    add_action( 'pre_get_posts', function ( $query ) {
        if ( is_admin() || ! $query->is_main_query() ||
            ! is_tax( 'your_tax' ) ) {
            return;
        }
    
        if ( ! empty( $_GET['pgs'] ) &&
            ( $paged = max( 1, $query->get( 'paged' ) ) ) &&
            // Runs only if the current page number exceeds the main query's max pages.
            $paged > $_GET['pgs']
        ) {
            $query->set( 'pg', $paged );          // for the custom query's pagination
            $query->set( 'paged', $_GET['pgs'] ); // for the main query's pagination
    
            // Prevent WordPress from redirecting to page/<max pages>.
            remove_action( 'template_redirect', 'redirect_canonical' );
        } else {
            $query->set( 'pg', $query->get( 'paged' ) );
        }
    } );
    
  2. Then in the bootstrap_pagination() function, apply the four ([1] to [4]) changes below:

    // [1] Add the $use_alt
    function bootstrap_pagination( \WP_Query $wp_query = NULL, $echo = TRUE, $use_alt = null ) {
    
        ...
    
        // [2] Add these:
        $add_args = [];
        if ( $use_alt ) {
            $add_args['pgs'] = $GLOBALS['wp_query']->max_num_pages;
        }
    
        $pages = paginate_links( [
                ...
                // [3] Use this instead.
                'current'      => max( 1, get_query_var( $use_alt ? 'pg' : 'paged' ) ),
                ...
                // [4] Use the $add_args
                'add_args'     => $add_args,
                ...
            ]
        );
    
        ...
    }
    
  3. Then in the archive/taxonomy.php template:

    // Define the $paged like so:
    $paged = max( 1, get_query_var( 'pg' ) );
    
    // ... your code here.
    
    // Then call bootstrap_pagination() like so:
    bootstrap_pagination( $the_query, true, true );
    

Tried & tested working, but obviously as you can see above, once the max num pages for the main query has been reached, the next pages will set the paged to that max num pages value. So it’s up to you how to not make that “look bad/weird/whatever” and to handle things like performance (if your custom query had 20 extra pages than the main query, then one would see the main query’s last page 20 times.. if they viewed them all).