Related posts loop based on tags AND categories doesn’t work without at least one tag assigned

You haven’t accounted for the case that there are no tags assigned in your declaration of $tag_related_posts. Your get_posts function always returns results in your line of code:

$tag_related_posts = get_posts('exclude=" . $post->ID . "&numberposts=" . $max_articles . "&tag=' . $tags_string);

If the $tags_string is null, it just returns a random list of posts and always fulfills the max post count, so your conditional to pull in by category never fires.

If you move your closing } for if($article_tags) from immediately after the foreach loop to after the if($tag_related_posts) block, your desired functionality appears it will kick in:

<?php
    $max_articles = 4;  // How many articles to display
    echo ' <div id="carousel-related-posts" class="owl-carousel owl-theme relatedposts">';

    $cnt = 0;


    // Show articles based on the same tag

    $article_tags = get_the_tags();
    $tags_string = '';
    if ($article_tags) {
        foreach ($article_tags as $article_tag) {
            $tags_string .= $article_tag->slug . ',';
        }

        $tag_related_posts = get_posts('exclude=" . $post->ID . "&numberposts=" . $max_articles . "&tag=' . $tags_string);

        if ($tag_related_posts) {
            foreach ($tag_related_posts as $related_post) {
                $cnt++; 
                $backgroundImg = wp_get_attachment_image_src( get_post_thumbnail_id($related_post->ID), 'medium_large' );?>

                         <div class="header-wrap" style="background: url('<?php echo $backgroundImg[0]; ?>') no-repeat 50% 30%; background-size: cover;"> 
                                <header class="entry-header">
                                    <div class="image-title">
                                        <?php incomplete_cat_list($related_post->ID); ?>
                                        <a class="post-title-link" href="https://wordpress.stackexchange.com/questions/288822/<?php the_permalink($related_post->ID); ?>">
                                            <?php print get_the_title($related_post->ID); ?>
                                        </a>
                                    </div>
                                </header>
                            </div>

            <?php } //end of foreach($article_tags as $article_tag)

        } //end of if($tag_related_posts)

    } //end of if($article_tags)


    // Only if there's not enough tag related articles,
    // add some from the same category

    if ($cnt < $max_articles) {

        $article_categories = get_the_category($post->ID);
        $category_string = '';
        foreach($article_categories as $category) { 
            $category_string .= $category->cat_ID . ',';
        }

        $cat_related_posts = get_posts('exclude=" . $post->ID . "&numberposts=" . $max_articles . "&category=' . $category_string);

        if ($cat_related_posts) {
            foreach ($cat_related_posts as $related_post) {
            $cnt++;
            if ($cnt > $max_articles) break;
            $backgroundImg = wp_get_attachment_image_src( get_post_thumbnail_id($related_post->ID), 'medium_large' );?>

                     <div class="header-wrap" style="background: url('<?php echo $backgroundImg[0]; ?>') no-repeat 50% 30%; background-size: cover;"> 
                            <header class="entry-header">
                                <div class="image-title">
                                    <?php incomplete_cat_list($related_post->ID); ?>
                                    <a class="post-title-link" href="https://wordpress.stackexchange.com/questions/288822/<?php the_permalink($related_post->ID); ?>">
                                        <?php print get_the_title($related_post->ID); ?>
                                    </a>
                                </div>
                            </header>
                        </div>

           <?php }
        }
    }

    echo '</div>';

?>