Check post on publish for blank title

Your error doesn’t appears on the screen because the page is reloaded after the action publish_post.

I have followed a different approach altogether, I’m checking the post title at wp_insert_post_data if the title is empty, I mark the post_status as draft and save a simple flag in options table. The flag I’ve saved is used to hide the Post Published notice and display a admin notice for specifying title in order to publish the post.

At the same moment I delete the option, so the notice is one time thing. You can modify it to a greater extent as per your requirements, but this is a basic solution and should work fine.

/**
 * Checks for empty post title, if empty sets the post status to draft
 *
 * @param $data
 * @param $postarr
 *
 * @return array
 */
function wse_279994_check_post_title( $data, $postarr ) {
    if ( is_array( $data ) && 'publish' == $data['post_status'] && empty( $data['post_title'] ) ) {
        $data['post_status'] = 'draft';
        update_option( 'wse_279994_post_error', 'empty_title' );
    }

    return $data;
}
add_filter( 'wp_insert_post_data', 'wse_279994_check_post_title', 10, 2 );

/**
 * If the post title was empty, do not show post published message
 */
add_filter( 'post_updated_messages', 'wse_279994_remove_all_messages' );

function wse_279994_remove_all_messages( $messages ) {
    if ( get_option( 'wse_279994_post_error' ) ) {
        return array();
    } else {
        return $messages;
    }
}

/**
 * Show admin notice for empty post title
 */
add_action( 'admin_notices', 'wse_279994_show_error' );
function wse_279994_show_error() {
    $screen = get_current_screen();
    if ( $screen->id != 'post' ) {
        return;
    }
    if ( ! get_option( 'wse_279994_post_error' ) ) {
        return;
    }
    echo '<div class="error"><p>' . esc_html__( "You need to enter a Post Title in order to publish it.", "wse" ) . '</p></div>';
    delete_option( 'wse_279994_post_error' );
}