Plugin

CrashHandler

Utility plugin for handling and possibly recovering from crashes without a restart

Utility Developers
index.ts
Download

Source

src/plugins/crashHandler/index.ts
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import { DataStore } from "@api/index";
8import { showNotification } from "@api/Notifications";
9import { definePluginSettings } from "@api/Settings";
10import { BRAND_NAME } from "@utils/branding";
11import { Devs } from "@utils/constants";
12import { Logger } from "@utils/Logger";
13import definePlugin, { OptionType } from "@utils/types";
14import { maybePromptToUpdate } from "@utils/updater";
15import { filters, findBulk, proxyLazyWebpack } from "@webpack";
16import { closeAllModals, DraftType, ExpressionPickerStore, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";
17
18const CrashHandlerLogger = new Logger("CrashHandler");
19
20const { ModalStack, DraftManager } = proxyLazyWebpack(() => {
21 const [ModalStack, DraftManager] = findBulk(
22 filters.byProps("pushLazy", "popAll"),
23 filters.byProps("clearDraft", "saveDraft"),
24 );
25
26 return {
27 ModalStack,
28 DraftManager
29 };
30});
31
32const settings = definePluginSettings({
33 attemptToPreventCrashes: {
34 type: OptionType.BOOLEAN,
35 description: "Whether to attempt to prevent Discord crashes.",
36 default: true
37 },
38 attemptToNavigateToHome: {
39 type: OptionType.BOOLEAN,
40 description: "Whether to attempt to navigate to the home when preventing Discord crashes.",
41 default: false
42 }
43});
44
45let hasCrashedOnce = false;
46let isRecovering = false;
47let shouldAttemptRecover = true;
48
49export default definePlugin({
50 name: "CrashHandler",
51 description: "Utility plugin for handling and possibly recovering from crashes without a restart",
52 authors: [Devs.Nuckyz],
53 tags: ["Utility", "Developers"],
54 enabledByDefault: true,
55 settings,
56
57 patches: [
58 {
59 find: "#{intl::ERRORS_UNEXPECTED_CRASH}",
60 replacement: {
61 match: /this\.setState\((.+?)\)/,
62 replace: "$self.handleCrash(this,$1);"
63 }
64 }
65 ],
66
67 handleCrash(_this: any, errorState: any) {
68 DataStore.del("KeepCurrentChannel_previousData");
69
70 if (IS_DEV) {
71 try {
72 if (errorState?.info && "componentStack" in errorState.info) {
73 console.error("Component Stack:", errorState.info.componentStack);
74 }
75 } catch { }
76 }
77 _this.setState(errorState);
78
79 // Already recovering, prevent error which happens more than once too fast to trigger another recover
80 if (isRecovering) return;
81 isRecovering = true;
82
83 // 1 ms timeout to avoid react breaking when re-rendering
84 setTimeout(() => {
85 try {
86 // Prevent a crash loop with an error that could not be handled
87 if (!shouldAttemptRecover) {
88 try {
89 showNotification({
90 color: "#eed202",
91 title: "Discord has crashed!",
92 body: "Awn :( Discord has crashed two times rapidly, not attempting to recover.",
93 noPersist: true
94 });
95 } catch { }
96
97 return;
98 }
99
100 shouldAttemptRecover = false;
101 // This is enough to avoid a crash loop
102 setTimeout(() => shouldAttemptRecover = true, 1000);
103 } catch { }
104
105 try {
106 if (!hasCrashedOnce) {
107 hasCrashedOnce = true;
108 maybePromptToUpdate(`Uh oh, Discord has just crashed... but good news, there is a ${BRAND_NAME} update available that might fix this issue! Would you like to update now?`, true);
109 }
110 } catch { }
111
112 try {
113 if (settings.store.attemptToPreventCrashes) {
114 this.handlePreventCrash(_this);
115 }
116 } catch (err) {
117 CrashHandlerLogger.error("Failed to handle crash", err);
118 }
119 }, 1);
120 },
121
122 handlePreventCrash(_this: any) {
123 try {
124 showNotification({
125 color: "#eed202",
126 title: "Discord has crashed!",
127 body: "Attempting to recover...",
128 noPersist: true
129 });
130 } catch { }
131
132 try {
133 const channelId = SelectedChannelStore.getChannelId();
134
135 for (const key in DraftType) {
136 if (!Number.isNaN(Number(key))) continue;
137
138 DraftManager.clearDraft(channelId, DraftType[key]);
139 }
140 } catch (err) {
141 CrashHandlerLogger.debug("Failed to clear drafts.", err);
142 }
143 try {
144 ExpressionPickerStore.closeExpressionPicker();
145 }
146 catch (err) {
147 CrashHandlerLogger.debug("Failed to close expression picker.", err);
148 }
149 try {
150 FluxDispatcher.dispatch({ type: "CONTEXT_MENU_CLOSE" });
151 } catch (err) {
152 CrashHandlerLogger.debug("Failed to close open context menu.", err);
153 }
154 try {
155 ModalStack.popAll();
156 } catch (err) {
157 CrashHandlerLogger.debug("Failed to close old modals.", err);
158 }
159 try {
160 closeAllModals();
161 } catch (err) {
162 CrashHandlerLogger.debug("Failed to close all open modals.", err);
163 }
164 try {
165 FluxDispatcher.dispatch({ type: "USER_PROFILE_MODAL_CLOSE" });
166 } catch (err) {
167 CrashHandlerLogger.debug("Failed to close user popout.", err);
168 }
169 try {
170 FluxDispatcher.dispatch({ type: "LAYER_POP_ALL" });
171 } catch (err) {
172 CrashHandlerLogger.debug("Failed to pop all layers.", err);
173 }
174 try {
175 FluxDispatcher.dispatch({
176 type: "DEV_TOOLS_SETTINGS_UPDATE",
177 settings: { displayTools: false, lastOpenTabId: "analytics" }
178 });
179 } catch (err) {
180 CrashHandlerLogger.debug("Failed to close DevTools.", err);
181 }
182
183 if (settings.store.attemptToNavigateToHome) {
184 try {
185 NavigationRouter.transitionToGuild("@me");
186 } catch (err) {
187 CrashHandlerLogger.debug("Failed to navigate to home", err);
188 }
189 }
190
191 // Set isRecovering to false before setting the state to allow us to handle the next crash error correcty, in case it happens
192 setImmediate(() => isRecovering = false);
193
194 try {
195 _this.setState({ error: null, info: null });
196 } catch (err) {
197 CrashHandlerLogger.debug("Failed to update crash handler component.", err);
198 }
199 }
200});
201