Plugin
CrashHandler
Utility plugin for handling and possibly recovering from crashes without a restart
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import { DataStore } from "@api/index";8
import { showNotification } from "@api/Notifications";9
import { definePluginSettings } from "@api/Settings";10
import { BRAND_NAME } from "@utils/branding";11
import { Devs } from "@utils/constants";12
import { Logger } from "@utils/Logger";13
import definePlugin, { OptionType } from "@utils/types";14
import { maybePromptToUpdate } from "@utils/updater";15
import { filters, findBulk, proxyLazyWebpack } from "@webpack";16
import { closeAllModals, DraftType, ExpressionPickerStore, FluxDispatcher, NavigationRouter, SelectedChannelStore } from "@webpack/common";17
18
const CrashHandlerLogger = new Logger("CrashHandler");19
20
const { 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
DraftManager29
};30
});31
32
const settings = definePluginSettings({33
attemptToPreventCrashes: {34
type: OptionType.BOOLEAN,35
description: "Whether to attempt to prevent Discord crashes.",36
default: true37
},38
attemptToNavigateToHome: {39
type: OptionType.BOOLEAN,40
description: "Whether to attempt to navigate to the home when preventing Discord crashes.",41
default: false42
}43
});44
45
let hasCrashedOnce = false;46
let isRecovering = false;47
let shouldAttemptRecover = true;48
49
export 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 recover80
if (isRecovering) return;81
isRecovering = true;82
83
// 1 ms timeout to avoid react breaking when re-rendering84
setTimeout(() => {85
try {86
// Prevent a crash loop with an error that could not be handled87
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: true94
});95
} catch { }96
97
return;98
}99
100
shouldAttemptRecover = false;101
// This is enough to avoid a crash loop102
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: true129
});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 happens192
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