diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css
index 893326ba38..0e8891ec0e 100644
--- a/src/wp-admin/css/list-tables.css
+++ b/src/wp-admin/css/list-tables.css
@@ -1301,6 +1301,31 @@ ul.cat-checklist {
 	text-decoration: underline;
 }
 
+.plugins tr.paused th.check-column {
+	border-left: 4px solid #d54e21;
+}
+
+.plugins tr.paused th,
+.plugins tr.paused td {
+	background-color: #fef7f1;
+}
+
+.plugins tr.paused .plugin-title,
+.plugins .paused .dashicons-warning {
+	color: #dc3232;
+}
+
+.plugins .paused .error-display p,
+.plugins .paused .error-display code {
+	font-size: 90%;
+	font-style: italic;
+	color: rgb( 0, 0, 0, 0.7 );
+}
+
+.plugins .resume-link {
+	color: #dc3232;
+}
+
 .plugin-card .update-now:before {
 	color: #f56e28;
 	content: "\f463";
diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php
index 0816b2420b..bc2f769b64 100644
--- a/src/wp-admin/includes/admin-filters.php
+++ b/src/wp-admin/includes/admin-filters.php
@@ -121,6 +121,7 @@
 add_action( 'load-themes.php', 'wp_theme_update_rows', 20 ); // After wp_update_themes() is called.
 
 add_action( 'admin_notices', 'update_nag', 3 );
+add_action( 'admin_notices', 'paused_plugins_notice', 5 );
 add_action( 'admin_notices', 'maintenance_nag', 10 );
 
 add_filter( 'update_footer', 'core_update_footer' );
diff --git a/src/wp-admin/includes/class-wp-plugins-list-table.php b/src/wp-admin/includes/class-wp-plugins-list-table.php
index 1f540c9ed2..e325fa0193 100644
--- a/src/wp-admin/includes/class-wp-plugins-list-table.php
+++ b/src/wp-admin/includes/class-wp-plugins-list-table.php
@@ -40,7 +40,7 @@ public function __construct( $args = array() ) {
 		);
 
 		$status = 'all';
-		if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search' ) ) ) {
+		if ( isset( $_REQUEST['plugin_status'] ) && in_array( $_REQUEST['plugin_status'], array( 'active', 'inactive', 'recently_activated', 'upgrade', 'mustuse', 'dropins', 'search', 'paused' ) ) ) {
 			$status = $_REQUEST['plugin_status'];
 		}
 
@@ -99,6 +99,7 @@ public function prepare_items() {
 			'upgrade'            => array(),
 			'mustuse'            => array(),
 			'dropins'            => array(),
+			'paused'             => array(),
 		);
 
 		$screen = $this->screen;
@@ -209,6 +210,9 @@ public function prepare_items() {
 				if ( $show_network_active ) {
 					// On the non-network screen, show network-active plugins if allowed
 					$plugins['active'][ $plugin_file ] = $plugin_data;
+					if ( is_plugin_paused( $plugin_file ) ) {
+						$plugins['paused'][ $plugin_file ] = $plugin_data;
+					}
 				} else {
 					// On the non-network screen, filter out network-active plugins
 					unset( $plugins['all'][ $plugin_file ] );
@@ -218,6 +222,9 @@ public function prepare_items() {
 				// On the non-network screen, populate the active list with plugins that are individually activated
 				// On the network-admin screen, populate the active list with plugins that are network activated
 				$plugins['active'][ $plugin_file ] = $plugin_data;
+				if ( is_plugin_paused( $plugin_file ) ) {
+					$plugins['paused'][ $plugin_file ] = $plugin_data;
+				}
 			} else {
 				if ( isset( $recently_activated[ $plugin_file ] ) ) {
 					// Populate the recently activated list with plugins that have been recently activated
@@ -421,24 +428,35 @@ protected function get_views() {
 
 			switch ( $type ) {
 				case 'all':
+					/* translators: %s: plugin count */
 					$text = _nx( 'All <span class="count">(%s)</span>', 'All <span class="count">(%s)</span>', $count, 'plugins' );
 					break;
 				case 'active':
+					/* translators: %s: plugin count */
 					$text = _n( 'Active <span class="count">(%s)</span>', 'Active <span class="count">(%s)</span>', $count );
 					break;
 				case 'recently_activated':
+					/* translators: %s: plugin count */
 					$text = _n( 'Recently Active <span class="count">(%s)</span>', 'Recently Active <span class="count">(%s)</span>', $count );
 					break;
 				case 'inactive':
+					/* translators: %s: plugin count */
 					$text = _n( 'Inactive <span class="count">(%s)</span>', 'Inactive <span class="count">(%s)</span>', $count );
 					break;
 				case 'mustuse':
+					/* translators: %s: plugin count */
 					$text = _n( 'Must-Use <span class="count">(%s)</span>', 'Must-Use <span class="count">(%s)</span>', $count );
 					break;
 				case 'dropins':
+					/* translators: %s: plugin count */
 					$text = _n( 'Drop-ins <span class="count">(%s)</span>', 'Drop-ins <span class="count">(%s)</span>', $count );
 					break;
+				case 'paused':
+					/* translators: %s: plugin count */
+					$text = _n( 'Paused <span class="count">(%s)</span>', 'Paused <span class="count">(%s)</span>', $count );
+					break;
 				case 'upgrade':
+					/* translators: %s: plugin count */
 					$text = _n( 'Update Available <span class="count">(%s)</span>', 'Update Available <span class="count">(%s)</span>', $count );
 					break;
 			}
@@ -649,6 +667,10 @@ public function single_row( $item ) {
 						/* translators: %s: plugin name */
 						$actions['deactivate'] = '<a href="' . wp_nonce_url( 'plugins.php?action=deactivate&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'deactivate-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Deactivate' ) . '</a>';
 					}
+					if ( current_user_can( 'resume_plugin' ) && is_plugin_paused( $plugin_file ) ) {
+						/* translators: %s: plugin name */
+						$actions['resume'] = '<a class="resume-link" href="' . wp_nonce_url( 'plugins.php?action=resume&amp;plugin=' . urlencode( $plugin_file ) . '&amp;plugin_status=' . $context . '&amp;paged=' . $page . '&amp;s=' . $s, 'resume-plugin_' . $plugin_file ) . '" aria-label="' . esc_attr( sprintf( _x( 'Resume execution of %s', 'plugin' ), $plugin_data['Name'] ) ) . '">' . __( 'Resume execution' ) . '</a>';
+					}
 				} else {
 					if ( current_user_can( 'activate_plugin', $plugin_file ) ) {
 						/* translators: %s: plugin name */
@@ -755,6 +777,11 @@ public function single_row( $item ) {
 			$class .= ' update';
 		}
 
+		$paused = is_plugin_paused( $plugin_file );
+		if ( $paused ) {
+			$class .= ' paused';
+		}
+
 		$plugin_slug = isset( $plugin_data['slug'] ) ? $plugin_data['slug'] : sanitize_title( $plugin_name );
 		printf(
 			'<tr class="%s" data-slug="%s" data-plugin="%s">',
@@ -833,12 +860,48 @@ public function single_row( $item ) {
 					 * @param array    $plugin_data An array of plugin data.
 					 * @param string   $status      Status of the plugin. Defaults are 'All', 'Active',
 					 *                              'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
-					 *                              'Drop-ins', 'Search'.
+					 *                              'Drop-ins', 'Search', 'Paused'
 					 */
 					$plugin_meta = apply_filters( 'plugin_row_meta', $plugin_meta, $plugin_file, $plugin_data, $status );
 					echo implode( ' | ', $plugin_meta );
 
-					echo '</div></td>';
+					echo '</div>';
+
+					if ( $paused ) {
+						echo sprintf(
+							'<p><span class="dashicons dashicons-warning"></span> <strong>%s</strong></p>',
+							__( 'This plugin failed to load properly and was paused within the admin backend.' )
+						);
+
+						$error = wp_get_plugin_error( $plugin_file );
+
+						if ( false !== $error ) {
+							$constants = get_defined_constants( true );
+							$constants = isset( $constants['Core'] ) ? $constants['Core'] : $constants['internal'];
+
+							foreach ( $constants as $constant => $value ) {
+								if ( 0 === strpos( $constant, 'E_' ) ) {
+									$core_errors[ $value ] = $constant;
+								}
+							}
+
+							$error['type'] = $core_errors[ $error['type'] ];
+
+							echo sprintf(
+								'<div class="error-display"><p>%s</p></div>',
+								sprintf(
+									/* translators: 1: error type, 2: error line number, 3: error file name, 4: error message */
+									__( 'The plugin caused an error of type %1$s in line %2$s of the file %3$s. Error message: %4$s' ),
+									"<code>{$error['type']}</code>",
+									"<code>{$error['line']}</code>",
+									"<code>{$error['file']}</code>",
+									"<code>{$error['message']}</code>"
+								)
+							);
+						}
+					}
+
+					echo '</td>';
 					break;
 				default:
 					$classes = "$column_name column-$column_name $class";
@@ -871,7 +934,7 @@ public function single_row( $item ) {
 		 * @param array  $plugin_data An array of plugin data.
 		 * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
 		 *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
-		 *                            'Drop-ins', 'Search'.
+		 *                            'Drop-ins', 'Search', 'Paused'.
 		 */
 		do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status );
 
@@ -887,7 +950,7 @@ public function single_row( $item ) {
 		 * @param array  $plugin_data An array of plugin data.
 		 * @param string $status      Status of the plugin. Defaults are 'All', 'Active',
 		 *                            'Inactive', 'Recently Activated', 'Upgrade', 'Must-Use',
-		 *                            'Drop-ins', 'Search'.
+		 *                            'Drop-ins', 'Search', 'Paused'
 		 */
 		do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status );
 	}
diff --git a/src/wp-admin/includes/plugin.php b/src/wp-admin/includes/plugin.php
index c898fc5169..0ebfc8830f 100644
--- a/src/wp-admin/includes/plugin.php
+++ b/src/wp-admin/includes/plugin.php
@@ -438,12 +438,14 @@ function get_dropins() {
  */
 function _get_dropins() {
 	$dropins = array(
-		'advanced-cache.php' => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE
-		'db.php'             => array( __( 'Custom database class.' ), true ), // auto on load
-		'db-error.php'       => array( __( 'Custom database error message.' ), true ), // auto on error
-		'install.php'        => array( __( 'Custom installation script.' ), true ), // auto on installation
-		'maintenance.php'    => array( __( 'Custom maintenance message.' ), true ), // auto on maintenance
-		'object-cache.php'   => array( __( 'External object cache.' ), true ), // auto on load
+		'advanced-cache.php'   => array( __( 'Advanced caching plugin.' ), 'WP_CACHE' ), // WP_CACHE
+		'db.php'               => array( __( 'Custom database class.' ), true ), // auto on load
+		'db-error.php'         => array( __( 'Custom database error message.' ), true ), // auto on error
+		'install.php'          => array( __( 'Custom installation script.' ), true ), // auto on installation
+		'maintenance.php'      => array( __( 'Custom maintenance message.' ), true ), // auto on maintenance
+		'object-cache.php'     => array( __( 'External object cache.' ), true ), // auto on load
+		'php-error.php'        => array( __( 'Custom PHP error message.' ), true ), // auto on error
+		'shutdown-handler.php' => array( __( 'Custom PHP shutdown handler.' ), true ), // auto on error
 	);
 
 	if ( is_multisite() ) {
@@ -496,6 +498,57 @@ function is_plugin_inactive( $plugin ) {
 	return ! is_plugin_active( $plugin );
 }
 
+/**
+ * Determines whether a plugin is technically active but was paused while
+ * loading.
+ *
+ * For more information on this and similar theme functions, check out
+ * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/
+ * Conditional Tags} article in the Theme Developer Handbook.
+ *
+ * @since 5.0.0
+ *
+ * @param string $plugin Path to the plugin file relative to the plugins directory.
+ * @return bool True, if in the list of paused plugins. False, not in the list.
+ */
+function is_plugin_paused( $plugin ) {
+	if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
+		return false;
+	}
+
+	if ( ! is_plugin_active( $plugin ) || is_plugin_active_for_network( $plugin ) ) {
+		return false;
+	}
+
+	list( $plugin ) = explode( '/', $plugin );
+
+	return array_key_exists( $plugin, $GLOBALS['_paused_plugins'] );
+}
+
+/**
+ * Gets the error that was recorded for a paused plugin.
+ *
+ * @since 5.0.0
+ *
+ * @param string $plugin Path to the plugin file relative to the plugins
+ *                       directory.
+ * @return array|false Array of error information as it was returned by
+ *                     `error_get_last()`, or false if none was recorded.
+ */
+function wp_get_plugin_error( $plugin ) {
+	if ( ! isset( $GLOBALS['_paused_plugins'] ) ) {
+		return false;
+	}
+
+	list( $plugin ) = explode( '/', $plugin );
+
+	if ( ! array_key_exists( $plugin, $GLOBALS['_paused_plugins'] ) ) {
+		return false;
+	}
+
+	return $GLOBALS['_paused_plugins'][ $plugin ];
+}
+
 /**
  * Determines whether the plugin is active for the entire network.
  *
@@ -693,6 +746,11 @@ function deactivate_plugins( $plugins, $silent = false, $network_wide = null ) {
 			continue;
 		}
 
+		// Clean up the database before deactivating the plugin.
+		if ( is_plugin_paused( $plugin ) ) {
+			resume_plugin( $plugin );
+		}
+
 		$network_deactivating = false !== $network_wide && is_plugin_active_for_network( $plugin );
 
 		if ( ! $silent ) {
@@ -887,6 +945,11 @@ function delete_plugins( $plugins, $deprecated = '' ) {
 			uninstall_plugin( $plugin_file );
 		}
 
+		// Clean up the database before removing the plugin.
+		if ( is_plugin_paused( $plugin_file ) ) {
+			resume_plugin( $plugin_file );
+		}
+
 		/**
 		 * Fires immediately before a plugin deletion attempt.
 		 *
@@ -959,6 +1022,62 @@ function delete_plugins( $plugins, $deprecated = '' ) {
 	return true;
 }
 
+/**
+ * Tries to resume a single plugin.
+ *
+ * Resuming the plugin basically means removing its entry from the
+ * `pause_on_admin` database option.
+ *
+ * If a redirect was provided, we first ensure the plugin does not throw fatal
+ * errors anymore.
+ *
+ * The way it works is by setting the redirection to the error before trying to
+ * include the plugin file. If the plugin fails, then the redirection will not
+ * be overwritten with the success message and the `pause_on_admin` option
+ * will not be updated.
+ *
+ * @since 5.0.0
+ *
+ * @param string $plugin   Single plugin to resume.
+ * @param string $redirect Optional. URL to redirect to.
+ *
+ * @return bool|WP_Error True on success, false if `$plugin` was not paused,
+ *                       `WP_Error` on failure.
+ */
+function resume_plugin( $plugin, $redirect = '' ) {
+	/*
+	 * We'll override this later if the plugin could be included without
+	 * creating a fatal error.
+	 */
+	if ( ! empty( $redirect ) ) {
+		wp_redirect(
+			add_query_arg(
+				'_error_nonce',
+				wp_create_nonce( 'plugin-resume-error_' . $plugin ),
+				$redirect
+			)
+		);
+
+		// Load the plugin to test whether it throws a fatal error.
+		ob_start();
+		$plugin_path = WP_PLUGIN_DIR . '/' . $plugin;
+		wp_register_plugin_realpath( $plugin_path );
+		include_once $plugin_path;
+		ob_clean();
+	}
+
+	$result = wp_forget_extension_error( 'plugins', $plugin );
+
+	if ( ! $result ) {
+		return new WP_Error(
+			'could_not_resume_plugin',
+			__( 'Could not resume execution of the plugin.' )
+		);
+	}
+
+	return true;
+}
+
 /**
  * Validate active plugins
  *
@@ -2066,3 +2185,35 @@ function wp_add_privacy_policy_content( $plugin_name, $policy_text ) {
 
 	WP_Privacy_Policy_Content::add( $plugin_name, $policy_text );
 }
+
+/**
+ * Renders an admin notice in case some plugins have been paused due to errors.
+ *
+ * @since 5.0.0
+ *
+ * @return void
+ */
+function paused_plugins_notice() {
+	if ( 'plugins.php' === $GLOBALS['pagenow'] ) {
+		return;
+	}
+
+	if ( ! current_user_can( 'deactivate_plugins' ) ) {
+		return;
+	}
+
+	if ( ! isset( $GLOBALS['_paused_plugins'] ) || empty( $GLOBALS['_paused_plugins'] ) ) {
+		return;
+	}
+
+	echo sprintf(
+		'<div class="notice notice-error"><p><strong>%s</strong><br>%s</p><p>%s</p></div>',
+		__( 'One or more plugins failed to load properly.' ),
+		__( 'You can find more details and make changes on the Plugins screen.' ),
+		sprintf(
+			'<a href="%s">%s</a>',
+			admin_url( 'plugins.php?plugin_status=paused' ),
+			'Go to the Plugins screen'
+		)
+	);
+}
diff --git a/src/wp-admin/plugins.php b/src/wp-admin/plugins.php
index 4d8c797b04..db0af2cc9b 100644
--- a/src/wp-admin/plugins.php
+++ b/src/wp-admin/plugins.php
@@ -389,6 +389,27 @@
 			}
 			break;
 
+		case 'resume':
+			if ( ! current_user_can( 'resume_plugin', $plugin ) ) {
+				wp_die( __( 'Sorry, you are not allowed to resume execution of this plugin.' ) );
+			}
+
+			if ( is_multisite() && ! is_network_admin() && is_network_only_plugin( $plugin ) ) {
+				wp_redirect( self_admin_url( "plugins.php?plugin_status=$status&paged=$page&s=$s" ) );
+				exit;
+			}
+
+			check_admin_referer( 'resume-plugin_' . $plugin );
+
+			$result = resume_plugin( $plugin, self_admin_url( 'plugins.php?error=resuming' ) );
+
+			if ( is_wp_error( $result ) ) {
+				wp_die( $result );
+			}
+
+			wp_redirect( self_admin_url( "plugins.php?resume=true&plugin_status=$status&paged=$page&s=$s" ) );
+			exit;
+
 		default:
 			if ( isset( $_POST['checked'] ) ) {
 				check_admin_referer( 'bulk-plugins' );
@@ -480,6 +501,8 @@
 		$errmsg = __( 'You cannot delete a plugin while it is active on the main site.' );
 	} elseif ( isset( $_GET['charsout'] ) ) {
 		$errmsg = sprintf( __( 'The plugin generated %d characters of <strong>unexpected output</strong> during activation. If you notice &#8220;headers already sent&#8221; messages, problems with syndication feeds or other issues, try deactivating or removing this plugin.' ), $_GET['charsout'] );
+	} elseif ( 'resuming' === $_GET['error'] ) {
+		$errmsg = __( 'Plugin could not be resumed because it triggered a <strong>fatal error</strong>.' );
 	} else {
 		$errmsg = __( 'Plugin could not be activated because it triggered a <strong>fatal error</strong>.' );
 	}
@@ -533,6 +556,8 @@
 	<div id="message" class="updated notice is-dismissible"><p><?php _e( 'Selected plugins <strong>deactivated</strong>.' ); ?></p></div>
 <?php elseif ( 'update-selected' == $action ) : ?>
 	<div id="message" class="updated notice is-dismissible"><p><?php _e( 'All selected plugins are up to date.' ); ?></p></div>
+<?php elseif ( isset( $_GET['resume'] ) ) : ?>
+	<div id="message" class="updated notice is-dismissible"><p><?php _e( 'Execution of plugin <strong>resumed</strong>.' ); ?></p></div>
 <?php endif; ?>
 
 <div class="wrap">
diff --git a/src/wp-includes/capabilities.php b/src/wp-includes/capabilities.php
index 375648e768..e65ff0b5a2 100644
--- a/src/wp-includes/capabilities.php
+++ b/src/wp-includes/capabilities.php
@@ -455,6 +455,7 @@ function map_meta_cap( $cap, $user_id ) {
 		case 'deactivate_plugins':
 		case 'activate_plugin':
 		case 'deactivate_plugin':
+		case 'resume_plugin':
 			$caps[] = 'activate_plugins';
 			if ( is_multisite() ) {
 				// update_, install_, and delete_ are handled above with is_super_admin().
diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php
index 79e445021b..345087fbf5 100644
--- a/src/wp-includes/load.php
+++ b/src/wp-includes/load.php
@@ -687,6 +687,36 @@ function wp_get_active_and_valid_plugins() {
 			$plugins[] = WP_PLUGIN_DIR . '/' . $plugin;
 		}
 	}
+
+	/*
+	 * Remove plugins from the list of active plugins when we're on an endpoint
+	 * that should be protected against WSODs and the plugin appears in the
+	 * `pause_on_admin` list.
+	 */
+	if ( is_protected_endpoint() ) {
+		$pause_on_admin = (array) get_option( 'pause_on_admin', array() );
+
+		if ( ! array_key_exists( 'plugins', $pause_on_admin ) ) {
+			return $plugins;
+		}
+
+		foreach ( $plugins as $index => $plugin ) {
+			$parts = explode(
+				'/',
+				str_replace( wp_normalize_path( WP_CONTENT_DIR . '/' ), '', wp_normalize_path( $plugin ) )
+			);
+
+			$type   = array_shift( $parts );
+			$plugin = array_shift( $parts );
+
+			if ( array_key_exists( $plugin, $pause_on_admin[ $type ] ) ) {
+				unset( $plugins[ $index ] );
+				// Store list of paused plugins for displaying an admin notice.
+				$GLOBALS['_paused_plugins'][ $plugin ] = $pause_on_admin[ $type ][ $plugin ];
+			}
+		}
+	}
+
 	return $plugins;
 }
 
@@ -1154,6 +1184,80 @@ function wp_doing_ajax() {
 	return apply_filters( 'wp_doing_ajax', defined( 'DOING_AJAX' ) && DOING_AJAX );
 }
 
+/**
+ * Determines whether we are currently on an endpoint that should be protected
+ * against WSODs.
+ *
+ * @since 5.0.0
+ *
+ * @return bool True if the current endpoint should be protected.
+ */
+function is_protected_endpoint() {
+	// Protect login pages.
+	if ( 'wp-login.php' === $GLOBALS['pagenow'] ) {
+		return true;
+	}
+
+	// Protect the admin backend.
+	if ( is_admin() && ! wp_doing_ajax() ) {
+		return true;
+	}
+
+	// Protect AJAX actions that could help resolve a fatal error should be available.
+	if ( wp_doing_ajax() && is_protected_ajax_action() ) {
+		return true;
+	}
+
+	/**
+	 * Filters whether the current request is against a protected endpoint.
+	 *
+	 * This filter is only fired when an endpoint is requested which is not already protected by
+	 * WordPress core. As such, it exclusively allows providing further protected endpoints in
+	 * addition to the admin backend, login pages and protected AJAX actions.
+	 *
+	 * @since 5.0.0
+	 *
+	 * @param bool $is_protected_endpoint Whether the currently requested endpoint is protected.
+	 *                                    Default false.
+	 */
+	return (bool) apply_filters( 'is_protected_endpoint', false );
+}
+
+/**
+ * Determines whether we are currently handling an AJAX action that should be
+ * protected against WSODs.
+ *
+ * @since 5.0.0
+ *
+ * @return bool True if the current AJAX action should be protected.
+ */
+function is_protected_ajax_action() {
+	$actions_to_protect = array(
+		'edit-theme-plugin-file', // Saving changes in the core code editor.
+		'heartbeat',              // Keep the heart beating.
+		'install-plugin',         // Installing a new plugin.
+		'install-theme',          // Installing a new theme.
+		'search-plugins',         // Searching in the list of plugins.
+		'search-install-plugins', // Searching for a plugin in the plugin install screen.
+		'update-plugin',          // Update an existing plugin.
+		'update-theme',           // Update an existing theme.
+	);
+
+	if ( ! wp_doing_ajax() ) {
+		return false;
+	}
+
+	if ( ! isset( $_REQUEST['action'] ) ) {
+		return false;
+	}
+
+	if ( ! in_array( $_REQUEST['action'], $actions_to_protect, true ) ) {
+		return false;
+	}
+
+	return true;
+}
+
 /**
  * Determines whether the current request is a WordPress cron request.
  *
@@ -1250,3 +1354,303 @@ function wp_finalize_scraping_edited_file_errors( $scrape_key ) {
 	}
 	echo "\n###### wp_scraping_result_end:$scrape_key ######\n";
 }
+
+/**
+ * Prunes the array of recorded extension errors.
+ *
+ * @since 5.0.0
+ *
+ * @param array $errors Array of errors to prune.
+ * @return array Pruned array of errors.
+ */
+function wp_prune_extension_errors( $errors ) {
+	foreach ( array( 'plugins', 'mu-plugins', 'themes' ) as $type ) {
+		if ( ! array_key_exists( $type, $errors ) ) {
+			continue;
+		}
+
+		switch ( $type ) {
+			case 'plugins':
+				$active_plugins = array_merge(
+					(array) get_option( 'active_plugins', array() ),
+					(array) get_option( 'active_sitewide_plugins', array() )
+				);
+
+				foreach ( $errors[ $type ] as $plugin => $error ) {
+					$found = false;
+
+					foreach ( $active_plugins as $active_plugin ) {
+						list( $active_plugin ) = explode( '/', $active_plugin );
+
+						if ( $active_plugin === $plugin ) {
+							$found = true;
+							break;
+						}
+					}
+
+					if ( ! $found ) {
+						unset( $errors[ $type ][ $plugin ] );
+					}
+				}
+
+				break;
+			case 'mu-plugins':
+				// TODO: Implement MU-plugin-specific behavior.
+				break;
+			case 'themes':
+				// TODO: Implement theme-specific behavior.
+				break;
+		}
+
+		if ( 0 === count( $errors[ $type ] ) ) {
+			unset( $errors[ $type ] );
+		}
+	}
+
+	return $errors;
+}
+
+/**
+ * Records the extension error as a database option.
+ *
+ * @since 5.0.0
+ *
+ * @global array $wp_theme_directories
+ *
+ * @param array $error Error that was triggered.
+ * @return bool Whether the error was correctly recorded.
+ */
+function wp_record_extension_error( $error ) {
+	global $wp_theme_directories;
+
+	if ( ! function_exists( 'get_option' ) ) {
+		return false;
+	}
+
+	$path = '';
+
+	$error_file      = wp_normalize_path( $error['file'] );
+	$wp_plugin_dir   = wp_normalize_path( WP_PLUGIN_DIR );
+	$wpmu_plugin_dir = wp_normalize_path( WPMU_PLUGIN_DIR );
+
+	if ( 0 === strpos( $error_file, $wp_plugin_dir ) ) {
+		$type = 'plugins';
+		$path = str_replace( $wp_plugin_dir . '/', '', $error_file );
+	} elseif ( 0 === strpos( $error_file, $wpmu_plugin_dir ) ) {
+		$type = 'mu-plugins';
+		$path = str_replace( $wpmu_plugin_dir . '/', '', $error_file );
+	} else {
+		foreach ( $wp_theme_directories as $theme_directory ) {
+			$theme_directory = wp_normalize_path( $theme_directory );
+			if ( 0 === strpos( $error_file, $theme_directory ) ) {
+				$type = 'themes';
+				$path = str_replace( $theme_directory . '/', '', $error_file );
+			}
+		}
+	}
+
+	if ( empty( $type ) || empty( $path ) ) {
+		return false;
+	}
+
+	$parts     = explode( '/', $path );
+	$extension = array_shift( $parts );
+
+	$errors = (array) get_option( 'pause_on_admin', array() );
+
+	$modified_errors = $errors;
+
+	if ( ! array_key_exists( $type, $modified_errors ) ) {
+		$modified_errors[ $type ] = array();
+	}
+
+	$modified_errors[ $type ][ $extension ] = $error;
+
+	$modified_errors = wp_prune_extension_errors( $modified_errors );
+
+	if ( $modified_errors === $errors ) {
+		return true;
+	}
+
+	return update_option( 'pause_on_admin', $modified_errors );
+}
+
+/**
+ * Forgets a previously recorded extension error again.
+ *
+ * @since 5.0.0
+ *
+ * @param string $type Type of the extension.
+ * @param string $extension Relative path of the extension.
+ * @return bool Whether the extension error was successfully forgotten.
+ */
+function wp_forget_extension_error( $type, $extension ) {
+	$errors = (array) get_option( 'pause_on_admin', array() );
+
+	if ( ! array_key_exists( $type, $errors ) ) {
+		return false;
+	}
+
+	$modified_errors = $errors;
+
+	switch ( $type ) {
+		case 'plugins':
+			list( $extension ) = explode( '/', $extension );
+	}
+
+	if ( array_key_exists( $extension, $modified_errors[ $type ] ) ) {
+		unset( $modified_errors[ $type ][ $extension ] );
+	}
+
+	$modified_errors = wp_prune_extension_errors( $modified_errors );
+
+	if ( $modified_errors === $errors ) {
+		return true;
+	}
+
+	return update_option( 'pause_on_admin', $modified_errors );
+}
+
+/**
+ * Determines whether we are dealing with an error that WordPress should handle
+ * in order to protect the admin backend against WSODs.
+ *
+ * @param array $error Error information retrieved from error_get_last().
+ *
+ * @return bool Whether WordPress should handle this error.
+ */
+function wp_should_handle_error( $error ) {
+	if ( ! isset( $error['type'] ) ) {
+		return false;
+	}
+
+	$error_types_to_handle = array(
+		E_ERROR,
+		E_PARSE,
+		E_USER_ERROR,
+		E_COMPILE_ERROR,
+		E_RECOVERABLE_ERROR,
+	);
+
+	return in_array( $error['type'], $error_types_to_handle, true );
+}
+
+/**
+ * Wraps the shutdown handler function so it can be made pluggable at a later
+ * stage.
+ *
+ * @since 5.0.0
+ *
+ * @return void
+ */
+function wp_shutdown_handler_wrapper() {
+	if ( defined( 'WP_EXECUTION_SUCCEEDED' ) && WP_EXECUTION_SUCCEEDED ) {
+		return;
+	}
+
+	// Load the pluggable shutdown handler in case we found one.
+	if ( function_exists( 'wp_handle_shutdown' ) ) {
+		$stop_propagation = false;
+
+		try {
+			$stop_propagation = (bool) wp_handle_shutdown();
+		} catch ( Exception $exception ) {
+			// Catch exceptions and remain silent.
+		}
+
+		if ( $stop_propagation ) {
+			return;
+		}
+	}
+
+	$error = error_get_last();
+
+	// No error, just skip the error handling code.
+	if ( null === $error ) {
+		return;
+	}
+
+	// Bail early if this error should not be handled.
+	if ( ! wp_should_handle_error( $error ) ) {
+		return;
+	}
+
+	try {
+		// Persist the detected error.
+		wp_record_extension_error( $error );
+
+		/*
+		 * If we happen to be on a protected endpoint, we try to redirect to
+		 * catch multiple errors in one go.
+		 */
+		if ( is_protected_endpoint() ) {
+			/*
+			 * Pluggable is usually loaded after plugins, so we manually
+			 * include it here for redirection functionality.
+			 */
+			if ( ! function_exists( 'wp_redirect' ) ) {
+				include ABSPATH . WPINC . '/pluggable.php';
+			}
+
+			$scheme = is_ssl() ? 'https://' : 'http://';
+
+			$url = "{$scheme}{$_SERVER['HTTP_HOST']}{$_SERVER['REQUEST_URI']}";
+			wp_redirect( $url );
+			exit;
+		}
+
+		// Load custom PHP error template, if present.
+		$php_error_pluggable = WP_CONTENT_DIR . '/php-error.php';
+		if ( is_readable( $php_error_pluggable ) ) {
+			/*
+			 * This drop-in should control the HTTP status code and print the
+			 * HTML markup indicating that a PHP error occurred. Alternatively,
+			 * `wp_die()` can be used.
+			 */
+			require_once $php_error_pluggable;
+			die();
+		}
+
+		// Otherwise, fall back to a default wp_die() message.
+		$message = sprintf(
+			'<p>%s</p>',
+			__( 'The site is experiencing technical difficulties.' )
+		);
+
+		if ( function_exists( 'get_admin_url' ) ) {
+			$message .= sprintf(
+				'<hr><p><em>%s <a href="%s">%s</a></em></p>',
+				__( 'Are you the site owner?' ),
+				get_admin_url(),
+				__( 'Log into the admin backend to fix this.' )
+			);
+		}
+
+		if ( function_exists( 'apply_filters' ) ) {
+			/**
+			 * Filters the message that the default PHP error page displays.
+			 *
+			 * @since 5.0.0
+			 *
+			 * @param string $message HTML error message to display.
+			 */
+			$message = apply_filters( 'wp_technical_issues_display', $message );
+		}
+
+		wp_die( $message, '', 500 );
+
+	} catch ( Exception $exception ) {
+		// Catch exceptions and remain silent.
+	}
+}
+
+/**
+ * Registers the WordPress premature shutdown handler.
+ *
+ * @since 5.0.0
+ *
+ * @return void
+ */
+function wp_register_premature_shutdown_handler() {
+	register_shutdown_function( 'wp_shutdown_handler_wrapper' );
+}
diff --git a/src/wp-settings.php b/src/wp-settings.php
index eb632238f0..c633f05b1d 100644
--- a/src/wp-settings.php
+++ b/src/wp-settings.php
@@ -20,6 +20,9 @@
 require( ABSPATH . WPINC . '/default-constants.php' );
 require_once( ABSPATH . WPINC . '/plugin.php' );
 
+// Make sure we register the premature shutdown handler as soon as possible.
+wp_register_premature_shutdown_handler();
+
 /*
  * These can't be directly globalized in version.php. When updating,
  * we're including version.php from another installation and don't want
@@ -40,6 +43,16 @@
 // Set initial default constants including WP_MEMORY_LIMIT, WP_MAX_MEMORY_LIMIT, WP_DEBUG, SCRIPT_DEBUG, WP_CONTENT_DIR and WP_CACHE.
 wp_initial_constants();
 
+/*
+ * Allow an optional shutdown handler to be included through a pluggable file.
+ * This file should register a function `wp_handle_shutdown( $context )` that
+ * returns a boolean value. If the return value evaluates to false, the default
+ * shutdown handler will not be executed.
+ */
+if ( is_readable( WP_CONTENT_DIR . '/shutdown-handler.php' ) ) {
+	include WP_CONTENT_DIR . '/shutdown-handler.php';
+}
+
 // Check for the required PHP version and for the MySQL extension or a database drop-in.
 wp_check_php_mysql_versions();
 
@@ -482,3 +495,12 @@
  * @since 3.0.0
  */
 do_action( 'wp_loaded' );
+
+/*
+ * Store the fact that we could successfully execute the entire WordPress
+ * lifecycle. This is used to skip the premature shutdown handler, as it cannot
+ * be unregistered.
+ */
+if ( ! defined( 'WP_EXECUTION_SUCCEEDED' ) ) {
+	define( 'WP_EXECUTION_SUCCEEDED', true );
+}
