Repository: electron/electron
Pull request: fix: `UtilityProcess.fork` crash before app ready

#### Description of Change

Closes https://github.com/electron/electron/issues/41396
Supersedes https://github.com/electron/electron/pull/41450

Shorter-term address for a crash that occurs when `UtilityProcess.fork` is called before the app ready event.

#### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] PR description included and stakeholders cc'd
- [x] `npm test` passes
- [x] tests are [changed or added](https://github.com/electron/electron/blob/main/docs/development/testing.md)
- [x] relevant API documentation, tutorials, and examples are updated and follow the [documentation style guide](https://github.com/electron/electron/blob/main/docs/development/style-guide.md)
- [x] [PR release notes](https://github.com/electron/clerk/blob/main/README.md) describe the change in a way relevant to app developers, and are [capitalized, punctuated, and past tense](https://github.com/electron/clerk/blob/main/README.md#examples).

#### Release Notes

Notes: Fixed an issue where calling `UtilityProcess.fork` prior to the app ready event would cause a crash.


Changed files:
filename: docs/api/utility-process.md
patch: @@ -44,6 +44,8 @@ Process: [Main](../glossary.md#main-process)<br />
 
 Returns [`UtilityProcess`](utility-process.md#class-utilityprocess)
 
+**Note:** `utilityProcess.fork` can only be called after the `ready` event has been emitted on `App`.
+
 ## Class: UtilityProcess
 
 > Instances of the `UtilityProcess` represent the Chromium spawned child process

filename: shell/browser/api/electron_api_utility_process.cc
patch: @@ -21,11 +21,13 @@
 #include "gin/object_template_builder.h"
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "shell/browser/api/message_port.h"
+#include "shell/browser/browser.h"
 #include "shell/browser/javascript_environment.h"
 #include "shell/browser/net/system_network_context_manager.h"
 #include "shell/common/gin_converters/callback_converter.h"
 #include "shell/common/gin_converters/file_path_converter.h"
 #include "shell/common/gin_helper/dictionary.h"
+#include "shell/common/gin_helper/error_thrower.h"
 #include "shell/common/gin_helper/object_template_builder.h"
 #include "shell/common/node_includes.h"
 #include "shell/common/v8_util.h"
@@ -412,6 +414,13 @@ raw_ptr<UtilityProcessWrapper> UtilityProcessWrapper::FromProcessId(
 // static
 gin::Handle<UtilityProcessWrapper> UtilityProcessWrapper::Create(
     gin::Arguments* args) {
+  if (!Browser::Get()->is_ready()) {
+    gin_helper::ErrorThrower(args->isolate())
+        .ThrowTypeError(
+            "utilityProcess cannot be created before app is ready.");
+    return {};
+  }
+
   gin_helper::Dictionary dict;
   if (!args->GetNext(&dict)) {
     args->ThrowTypeError("Options must be an object.");

filename: spec/fixtures/crash-cases/utility-process-app-ready/index.js
patch: @@ -0,0 +1,31 @@
+const { app, BrowserWindow, utilityProcess } = require('electron');
+
+const path = require('node:path');
+
+function createWindow () {
+  const mainWindow = new BrowserWindow();
+  mainWindow.loadFile('about:blank');
+}
+
+app.whenReady().then(() => {
+  createWindow();
+
+  app.on('activate', function () {
+    if (BrowserWindow.getAllWindows().length === 0) createWindow();
+  });
+});
+
+app.on('window-all-closed', function () {
+  if (process.platform !== 'darwin') app.quit();
+});
+
+try {
+  utilityProcess.fork(path.join(__dirname, 'utility.js'));
+} catch (e) {
+  if (/utilityProcess cannot be created before app is ready/.test(e.message)) {
+    app.exit(0);
+  } else {
+    console.error(e);
+    app.exit(1);
+  }
+}