refactor: add trycatch util (@miodec) (#6492)

Adds trycatch util to cleanup try catch code.
This commit is contained in:
Jack 2025-04-26 21:24:39 +02:00 committed by GitHub
parent a59f99a533
commit e06f7f41cf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 331 additions and 203 deletions

View file

@ -63,6 +63,7 @@ import {
checkCompatibility,
stringToFunboxNames,
} from "@monkeytype/funbox";
import { tryCatch } from "@monkeytype/util/trycatch";
try {
if (!anticheatImplemented()) throw new Error("undefined");
@ -318,13 +319,7 @@ export async function addResult(
// );
// return res.status(400).json({ message: "Time traveler detected" });
//get latest result ordered by timestamp
let lastResultTimestamp: null | number = null;
try {
lastResultTimestamp = (await ResultDAL.getLastResult(uid)).timestamp;
} catch (e) {
//
}
const { data: lastResult } = await tryCatch(ResultDAL.getLastResult(uid));
//convert result test duration to miliseconds
completedEvent.timestamp = Math.floor(Date.now() / 1000) * 1000;
@ -333,13 +328,13 @@ export async function addResult(
const testDurationMilis = completedEvent.testDuration * 1000;
const incompleteTestsMilis = completedEvent.incompleteTestSeconds * 1000;
const earliestPossible =
(lastResultTimestamp ?? 0) + testDurationMilis + incompleteTestsMilis;
(lastResult?.timestamp ?? 0) + testDurationMilis + incompleteTestsMilis;
const nowNoMilis = Math.floor(Date.now() / 1000) * 1000;
if (lastResultTimestamp && nowNoMilis < earliestPossible - 1000) {
if (lastResult?.timestamp && nowNoMilis < earliestPossible - 1000) {
void addLog(
"invalid_result_spacing",
{
lastTimestamp: lastResultTimestamp,
lastTimestamp: lastResult.timestamp,
earliestPossible,
now: nowNoMilis,
testDuration: testDurationMilis,
@ -786,17 +781,16 @@ async function calculateXp(
const accuracyModifier = (acc - 50) / 50;
let dailyBonus = 0;
let lastResultTimestamp: number | undefined;
const { data: lastResult, error: getLastResultError } = await tryCatch(
ResultDAL.getLastResult(uid)
);
try {
const { timestamp } = await ResultDAL.getLastResult(uid);
lastResultTimestamp = timestamp;
} catch (err) {
Logger.error(`Could not fetch last result: ${err}`);
if (getLastResultError) {
Logger.error(`Could not fetch last result: ${getLastResultError}`);
}
if (lastResultTimestamp) {
const lastResultDay = getStartOfDayTimestamp(lastResultTimestamp);
if (lastResult?.timestamp) {
const lastResultDay = getStartOfDayTimestamp(lastResult.timestamp);
const today = getCurrentDayTimestamp();
if (lastResultDay !== today) {
const proportionalXp = Math.round(currentTotalXp * 0.05);

View file

@ -88,13 +88,11 @@ import {
} from "@monkeytype/contracts/users";
import { MILLISECONDS_IN_DAY } from "@monkeytype/util/date-and-time";
import { MonkeyRequest } from "../types";
import { tryCatch } from "@monkeytype/util/trycatch";
async function verifyCaptcha(captcha: string): Promise<void> {
let verified = false;
try {
verified = await verify(captcha);
} catch (e) {
//fetch to recaptcha api can sometimes fail
const { data: verified, error } = await tryCatch(verify(captcha));
if (error) {
throw new MonkeyError(
422,
"Request to the Captcha API failed, please try again later"
@ -177,18 +175,19 @@ export async function sendVerificationEmail(
);
}
let link = "";
try {
link = await FirebaseAdmin()
const { data: link, error } = await tryCatch(
FirebaseAdmin()
.auth()
.generateEmailVerificationLink(email, {
url: isDevEnvironment()
? "http://localhost:3000"
: "https://monkeytype.com",
});
} catch (e) {
if (isFirebaseError(e)) {
if (e.errorInfo.code === "auth/user-not-found") {
})
);
if (error) {
if (isFirebaseError(error)) {
if (error.errorInfo.code === "auth/user-not-found") {
throw new MonkeyError(
500,
"Auth user not found when the user was found in the database. Contact support with this error message and your email",
@ -198,11 +197,11 @@ export async function sendVerificationEmail(
}),
userInfo.uid
);
} else if (e.errorInfo.code === "auth/too-many-requests") {
} else if (error.errorInfo.code === "auth/too-many-requests") {
throw new MonkeyError(429, "Too many requests. Please try again later");
} else if (
e.errorInfo.code === "auth/internal-error" &&
e.errorInfo.message.toLowerCase().includes("too_many_attempts")
error.errorInfo.code === "auth/internal-error" &&
error.errorInfo.message.toLowerCase().includes("too_many_attempts")
) {
throw new MonkeyError(
429,
@ -212,12 +211,12 @@ export async function sendVerificationEmail(
throw new MonkeyError(
500,
"Firebase failed to generate an email verification link: " +
e.errorInfo.message,
JSON.stringify(e)
error.errorInfo.message,
JSON.stringify(error)
);
}
} else {
const message = getErrorMessage(e);
const message = getErrorMessage(error);
if (message === undefined) {
throw new MonkeyError(
500,
@ -233,12 +232,13 @@ export async function sendVerificationEmail(
throw new MonkeyError(
500,
"Failed to generate an email verification link: " + message,
(e as Error).stack
error.stack
);
}
}
}
}
await emailQueue.sendVerificationEmail(email, userInfo.name, link);
return new MonkeyResponse("Email sent", null);
@ -259,22 +259,20 @@ export async function sendForgotPasswordEmail(
export async function deleteUser(req: MonkeyRequest): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
let userInfo:
| Pick<UserDAL.DBUser, "banned" | "name" | "email" | "discordId">
| undefined;
try {
userInfo = await UserDAL.getPartialUser(uid, "delete user", [
const { data: userInfo, error } = await tryCatch(
UserDAL.getPartialUser(uid, "delete user", [
"banned",
"name",
"email",
"discordId",
]);
} catch (e) {
if (e instanceof MonkeyError && e.status === 404) {
])
);
if (error) {
if (error instanceof MonkeyError && error.status === 404) {
//userinfo was already deleted. We ignore this and still try to remove the other data
} else {
throw e;
throw error;
}
}
@ -533,12 +531,12 @@ function getRelevantUserInfo(user: UserDAL.DBUser): RelevantUserInfo {
export async function getUser(req: MonkeyRequest): Promise<GetUserResponse> {
const { uid } = req.ctx.decodedToken;
let userInfo: UserDAL.DBUser;
try {
userInfo = await UserDAL.getUser(uid, "get user");
} catch (e) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (e.status === 404) {
const { data: userInfo, error } = await tryCatch(
UserDAL.getUser(uid, "get user")
);
if (error) {
if (error instanceof MonkeyError && error.status === 404) {
//if the user is in the auth system but not in the db, its possible that the user was created by bypassing captcha
//since there is no data in the database anyway, we can just delete the user from the auth system
//and ask them to sign up again
@ -564,7 +562,7 @@ export async function getUser(req: MonkeyRequest): Promise<GetUserResponse> {
}
}
} else {
throw e;
throw error;
}
}

View file

@ -3,13 +3,17 @@ import { getMiddleware as getSwaggerMiddleware } from "swagger-stats";
import { isDevEnvironment } from "../../utils/misc";
import { readFileSync } from "fs";
import Logger from "../../utils/logger";
import { tryCatchSync } from "@monkeytype/util/trycatch";
function addSwaggerMiddlewares(app: Application): void {
const openApiSpec = __dirname + "/../../../dist/static/api/openapi.json";
let spec = {};
try {
spec = JSON.parse(readFileSync(openApiSpec, "utf8")) as string;
} catch (err) {
const { data: spec, error } = tryCatchSync(
() =>
JSON.parse(readFileSync(openApiSpec, "utf8")) as Record<string, unknown>
);
if (error) {
Logger.warning(
`Cannot read openApi specification from ${openApiSpec}. Swagger stats will not fully work.`
);
@ -21,7 +25,7 @@ function addSwaggerMiddlewares(app: Application): void {
uriPath: "/stats",
authentication: !isDevEnvironment(),
apdexThreshold: 100,
swaggerSpec: spec,
swaggerSpec: spec ?? {},
onAuthenticate: (_req, username, password) => {
return (
username === process.env["STATS_USERNAME"] &&

View file

@ -1,4 +1,4 @@
import { SimpleGit, simpleGit } from "simple-git";
import { simpleGit } from "simple-git";
import { Collection, ObjectId } from "mongodb";
import path from "path";
import { existsSync, writeFileSync } from "fs";
@ -10,6 +10,7 @@ import { ApproveQuote, Quote } from "@monkeytype/contracts/schemas/quotes";
import { WithObjectId } from "../utils/misc";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
import { z } from "zod";
import { tryCatchSync } from "@monkeytype/util/trycatch";
const JsonQuoteSchema = z.object({
text: z.string(),
@ -28,12 +29,12 @@ const QuoteDataSchema = z.object({
const PATH_TO_REPO = "../../../../monkeytype-new-quotes";
let git: SimpleGit | undefined;
try {
git = simpleGit(path.join(__dirname, PATH_TO_REPO));
} catch (e) {
console.error(`Failed to initialize git: ${e}`);
git = undefined;
const { data: git, error } = tryCatchSync(() =>
simpleGit(path.join(__dirname, PATH_TO_REPO))
);
if (error) {
console.error(`Failed to initialize git: ${error}`);
}
type AddQuoteReturn = {
@ -145,7 +146,7 @@ export async function approve(
editSource: string | undefined,
name: string
): Promise<ApproveReturn> {
if (git === undefined) throw new MonkeyError(500, "Git not available.");
if (git === null) throw new MonkeyError(500, "Git not available.");
//check mod status
const targetQuote = await getNewQuoteCollection().findOne({
_id: new ObjectId(quoteId),

View file

@ -8,8 +8,9 @@ import {
import MonkeyError from "../utils/error";
import * as db from "../init/db";
import { getUser, getTags, DBUser } from "./user";
import { getUser, getTags } from "./user";
import { DBResult } from "../utils/result";
import { tryCatch } from "@monkeytype/util/trycatch";
export const getResultCollection = (): Collection<DBResult> =>
db.collection<DBResult>("results");
@ -18,12 +19,8 @@ export async function addResult(
uid: string,
result: DBResult
): Promise<{ insertedId: ObjectId }> {
let user: DBUser | null = null;
try {
user = await getUser(uid, "add result");
} catch (e) {
user = null;
}
const { data: user } = await tryCatch(getUser(uid, "add result"));
if (!user) throw new MonkeyError(404, "User not found", "add result");
if (result.uid === undefined) result.uid = uid;
// result.ir = true;

View file

@ -8,6 +8,7 @@ import { recordEmail } from "../utils/prometheus";
import type { EmailTaskContexts, EmailType } from "../queues/email-queue";
import { isDevEnvironment } from "../utils/misc";
import { getErrorMessage } from "../utils/error";
import { tryCatch } from "@monkeytype/util/trycatch";
type EmailMetadata = {
subject: string;
@ -109,14 +110,15 @@ export async function sendEmail(
type Result = { response: string; accepted: string[] };
let result: Result;
try {
result = (await transporter.sendMail(mailOptions)) as Result;
} catch (e) {
const { data: result, error } = await tryCatch(
transporter.sendMail(mailOptions) as Promise<Result>
);
if (error) {
recordEmail(templateName, "fail");
return {
success: false,
message: getErrorMessage(e) ?? "Unknown error",
message: getErrorMessage(error) ?? "Unknown error",
};
}

View file

@ -11,6 +11,7 @@ import { getCurrentWeekTimestamp } from "@monkeytype/util/date-and-time";
import MonkeyError from "../utils/error";
import { omit } from "lodash";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
import { tryCatchSync } from "@monkeytype/util/trycatch";
type AddResultOpts = {
entry: RedisXpLeaderboardEntry;
@ -225,11 +226,11 @@ export class WeeklyXpLeaderboard {
return null;
}
// safely parse the result with error handling
let parsed: RedisXpLeaderboardEntry;
try {
parsed = parseJsonWithSchema(result, RedisXpLeaderboardEntrySchema);
} catch (error) {
const { data: parsed, error } = tryCatchSync(() =>
parseJsonWithSchema(result, RedisXpLeaderboardEntrySchema)
);
if (error) {
throw new MonkeyError(
500,
`Failed to parse leaderboard entry: ${

View file

@ -19,6 +19,7 @@ import {
} from "@monkeytype/contracts/schemas/configs";
import { Mode } from "@monkeytype/contracts/schemas/shared";
import { CompletedEvent } from "@monkeytype/contracts/schemas/results";
import { tryCatch } from "@monkeytype/util/trycatch";
let challengeLoading = false;
@ -219,11 +220,9 @@ export async function setup(challengeName: string): Promise<boolean> {
UpdateConfig.setFunbox("none");
let list;
try {
list = await JSONData.getChallengeList();
} catch (e) {
const message = Misc.createErrorMessage(e, "Failed to setup challenge");
const { data: list, error } = await tryCatch(JSONData.getChallengeList());
if (error) {
const message = Misc.createErrorMessage(error, "Failed to setup challenge");
Notifications.add(message, -1);
ManualRestart.set();
setTimeout(() => {

View file

@ -41,6 +41,7 @@ import {
isFunboxActiveWithProperty,
getActiveFunboxNames,
} from "../test/funbox/list";
import { tryCatchSync } from "@monkeytype/util/trycatch";
let dontInsertSpace = false;
let correctShiftUsed = true;
@ -344,18 +345,16 @@ async function handleSpace(): Promise<void> {
TestState.activeWordIndex - TestUI.activeWordElementOffset - 1
]?.offsetTop ?? 0
);
let nextTop: number;
try {
nextTop = Math.floor(
const { data: nextTop } = tryCatchSync(() =>
Math.floor(
document.querySelectorAll<HTMLElement>("#words .word")[
TestState.activeWordIndex - TestUI.activeWordElementOffset
]?.offsetTop ?? 0
);
} catch (e) {
nextTop = 0;
}
)
);
if (nextTop > currentTop) {
if ((nextTop ?? 0) > currentTop) {
void TestUI.lineJump(currentTop);
} //end of line wrap
}

View file

@ -4,6 +4,7 @@ import { cachedFetchJson } from "../utils/json-data";
import { subscribe } from "../observables/config-event";
import * as DB from "../db";
import Ape from "../ape";
import { tryCatch } from "@monkeytype/util/trycatch";
export type Quote = {
text: string;
@ -59,20 +60,19 @@ class QuotesController {
const normalizedLanguage = removeLanguageSize(language);
if (this.quoteCollection.language !== normalizedLanguage) {
let data: QuoteData;
try {
data = await cachedFetchJson<QuoteData>(
`quotes/${normalizedLanguage}.json`
);
} catch (e) {
const { data, error } = await tryCatch(
cachedFetchJson<QuoteData>(`quotes/${normalizedLanguage}.json`)
);
if (error) {
if (
e instanceof Error &&
(e?.message?.includes("404") ||
e?.message?.includes("Content is not JSON"))
error instanceof Error &&
(error?.message?.includes("404") ||
error?.message?.includes("Content is not JSON"))
) {
return defaultQuoteCollection;
} else {
throw e;
throw error;
}
}

View file

@ -12,6 +12,7 @@ import * as Notifications from "../elements/notifications";
import * as Loader from "../elements/loader";
import * as AnalyticsController from "../controllers/analytics-controller";
import { debounce } from "throttle-debounce";
import { tryCatch } from "@monkeytype/util/trycatch";
export let randomTheme: string | null = null;
let isPreviewingTheme = false;
@ -248,12 +249,10 @@ export async function clearPreview(applyTheme = true): Promise<void> {
let themesList: string[] = [];
async function changeThemeList(): Promise<void> {
let themes;
try {
themes = await JSONData.getThemesList();
} catch (e) {
const { data: themes, error } = await tryCatch(JSONData.getThemesList());
if (error) {
console.error(
Misc.createErrorMessage(e, "Failed to update random theme list")
Misc.createErrorMessage(error, "Failed to update random theme list")
);
return;
}

View file

@ -18,6 +18,7 @@ import { LocalStorageWithSchema } from "../../utils/local-storage-with-schema";
import defaultResultFilters from "../../constants/default-result-filters";
import { getAllFunboxes } from "@monkeytype/funbox";
import { SnapshotUserTag } from "../../constants/default-snapshot";
import { tryCatch } from "@monkeytype/util/trycatch";
export function mergeWithDefaultFilters(
filters: Partial<ResultFilters>
@ -745,14 +746,16 @@ export async function appendButtons(
): Promise<void> {
selectChangeCallbackFn = selectChangeCallback;
let languageList;
try {
languageList = await JSONData.getLanguageList();
} catch (e) {
const { data: languageList, error } = await tryCatch(
JSONData.getLanguageList()
);
if (error) {
console.error(
Misc.createErrorMessage(e, "Failed to append language buttons")
Misc.createErrorMessage(error, "Failed to append language buttons")
);
}
if (languageList) {
let html = "";

View file

@ -12,6 +12,7 @@ import * as ConfigEvent from "../../observables/config-event";
import { isAuthenticated } from "../../firebase";
import * as ActivePage from "../../states/active-page";
import { CustomThemeColors } from "@monkeytype/contracts/schemas/configs";
import { tryCatch } from "@monkeytype/util/trycatch";
function updateActiveButton(): void {
let activeThemeName = Config.theme;
@ -172,12 +173,13 @@ export async function refreshButtons(): Promise<void> {
activeThemeName = ThemeController.randomTheme;
}
let themes;
try {
themes = await JSONData.getSortedThemesList();
} catch (e) {
const { data: themes, error } = await tryCatch(
JSONData.getSortedThemesList()
);
if (error) {
Notifications.add(
Misc.createErrorMessage(e, "Failed to refresh theme buttons"),
Misc.createErrorMessage(error, "Failed to refresh theme buttons"),
-1
);
return;

View file

@ -8,6 +8,7 @@ import AnimatedModal, {
ShowOptions,
} from "../utils/animated-modal";
import { LayoutsList } from "../constants/layouts";
import { tryCatch } from "@monkeytype/util/trycatch";
type FilterPreset = {
display: string;
@ -98,19 +99,17 @@ const presets: Record<string, FilterPreset> = {
async function initSelectOptions(): Promise<void> {
$("#wordFilterModal .languageInput").empty();
$("#wordFilterModal .layoutInput").empty();
$("wordFilterModal .presetInput").empty();
let LanguageList;
const { data: LanguageList, error } = await tryCatch(
JSONData.getLanguageList()
);
try {
LanguageList = await JSONData.getLanguageList();
} catch (e) {
if (error) {
console.error(
Misc.createErrorMessage(
e,
error,
"Failed to initialise word filter popup language list"
)
);
@ -187,12 +186,12 @@ async function filter(language: string): Promise<string[]> {
const regexcl = new RegExp(filterout, "i");
const filteredWords = [];
let languageWordList;
try {
languageWordList = await JSONData.getLanguage(language);
} catch (e) {
const { data: languageWordList, error } = await tryCatch(
JSONData.getLanguage(language)
);
if (error) {
Notifications.add(
Misc.createErrorMessage(e, "Failed to filter language words"),
Misc.createErrorMessage(error, "Failed to filter language words"),
-1
);
return [];

View file

@ -12,6 +12,7 @@ import {
SpeedHistogram,
} from "@monkeytype/contracts/schemas/public";
import { getNumberWithMagnitude, numberWithSpaces } from "../utils/numbers";
import { tryCatch } from "@monkeytype/util/trycatch";
function reset(): void {
$(".pageAbout .contributors").empty();
@ -126,26 +127,24 @@ async function getStatsAndHistogramData(): Promise<void> {
}
async function fill(): Promise<void> {
let supporters: string[];
try {
supporters = await JSONData.getSupportersList();
} catch (e) {
const { data: supporters, error: supportersError } = await tryCatch(
JSONData.getSupportersList()
);
if (supportersError) {
Notifications.add(
Misc.createErrorMessage(e, "Failed to get supporters"),
Misc.createErrorMessage(supportersError, "Failed to get supporters"),
-1
);
supporters = [];
}
let contributors: string[];
try {
contributors = await JSONData.getContributorsList();
} catch (e) {
const { data: contributors, error: contributorsError } = await tryCatch(
JSONData.getContributorsList()
);
if (contributorsError) {
Notifications.add(
Misc.createErrorMessage(e, "Failed to get contributors"),
Misc.createErrorMessage(contributorsError, "Failed to get contributors"),
-1
);
contributors = [];
}
void getStatsAndHistogramData().then(() => {
@ -154,7 +153,7 @@ async function fill(): Promise<void> {
const supportersEl = document.querySelector(".pageAbout .supporters");
let supportersHTML = "";
for (const supporter of supporters) {
for (const supporter of supporters ?? []) {
supportersHTML += `<div>${supporter}</div>`;
}
if (supportersEl) {
@ -163,7 +162,7 @@ async function fill(): Promise<void> {
const contributorsEl = document.querySelector(".pageAbout .contributors");
let contributorsHTML = "";
for (const contributor of contributors) {
for (const contributor of contributors ?? []) {
contributorsHTML += `<div>${contributor}</div>`;
}
if (contributorsEl) {

View file

@ -33,6 +33,7 @@ import { getActiveFunboxNames } from "../test/funbox/list";
import { SnapshotPreset } from "../constants/default-snapshot";
import { LayoutsList } from "../constants/layouts";
import { DataArrayPartial, Optgroup } from "slim-select/store";
import { tryCatch } from "@monkeytype/util/trycatch";
type SettingsGroups<T extends ConfigValue> = Record<string, SettingsGroup<T>>;
@ -461,13 +462,13 @@ async function fillSettingsPage(): Promise<void> {
// Language Selection Combobox
let languageGroups;
try {
languageGroups = await JSONData.getLanguageGroups();
} catch (e) {
const { data: languageGroups, error: getLanguageGroupsError } =
await tryCatch(JSONData.getLanguageGroups());
if (getLanguageGroupsError) {
console.error(
Misc.createErrorMessage(
e,
getLanguageGroupsError,
"Failed to initialize settings language picker"
)
);
@ -522,12 +523,15 @@ async function fillSettingsPage(): Promise<void> {
select: keymapLayoutSelectElement,
});
let themes;
try {
themes = await JSONData.getThemesList();
} catch (e) {
const { data: themes, error: getThemesListError } = await tryCatch(
JSONData.getThemesList()
);
if (getThemesListError) {
console.error(
Misc.createErrorMessage(e, "Failed to load themes into dropdown boxes")
Misc.createErrorMessage(
getThemesListError,
"Failed to load themes into dropdown boxes"
)
);
}
@ -625,12 +629,15 @@ async function fillSettingsPage(): Promise<void> {
let fontsElHTML = "";
let fontsList;
try {
fontsList = await JSONData.getFontsList();
} catch (e) {
const { data: fontsList, error: getFontsListError } = await tryCatch(
JSONData.getFontsList()
);
if (getFontsListError) {
console.error(
Misc.createErrorMessage(e, "Failed to update fonts settings buttons")
Misc.createErrorMessage(
getFontsListError,
"Failed to update fonts settings buttons"
)
);
}

View file

@ -18,6 +18,7 @@ import {
getActiveFunboxesWithProperty,
} from "./list";
import { checkForcedConfig } from "./funbox-validation";
import { tryCatch } from "@monkeytype/util/trycatch";
export function toggleScript(...params: string[]): void {
if (Config.funbox === "none") return;
@ -118,12 +119,12 @@ export async function activate(funbox?: string): Promise<boolean | undefined> {
$("#wordsWrapper").removeClass("hidden");
let language;
try {
language = await JSONData.getCurrentLanguage(Config.language);
} catch (e) {
const { data: language, error } = await tryCatch(
JSONData.getCurrentLanguage(Config.language)
);
if (error) {
Notifications.add(
Misc.createErrorMessage(e, "Failed to activate funbox"),
Misc.createErrorMessage(error, "Failed to activate funbox"),
-1
);
UpdateConfig.setFunbox("none", true);

View file

@ -32,8 +32,8 @@ export async function getCharFromEvent(
return altVersion || nonAltVersion || defaultVersion;
}
let layout;
let layout;
try {
layout = await JSONData.getLayout(Config.layout);
} catch (e) {

View file

@ -74,6 +74,7 @@ import { getFunboxesFromString } from "@monkeytype/funbox";
import * as CompositionState from "../states/composition";
import { SnapshotResult } from "../constants/default-snapshot";
import { WordGenError } from "../utils/word-gen-error";
import { tryCatch } from "@monkeytype/util/trycatch";
let failReason = "";
const koInputVisual = document.getElementById("koInputVisual") as HTMLElement;
@ -406,12 +407,12 @@ export async function init(): Promise<void> {
TestInput.input.resetHistory();
TestInput.input.current = "";
let language;
try {
language = await JSONData.getLanguage(Config.language);
} catch (e) {
const { data: language, error } = await tryCatch(
JSONData.getLanguage(Config.language)
);
if (error) {
Notifications.add(
Misc.createErrorMessage(e, "Failed to load language"),
Misc.createErrorMessage(error, "Failed to load language"),
-1
);
}

View file

@ -4,6 +4,7 @@ import * as Strings from "../utils/strings";
import * as JSONData from "../utils/json-data";
import { z } from "zod";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
import { tryCatch } from "@monkeytype/util/trycatch";
export async function getTLD(
languageGroup: JSONData.LanguageGroup
@ -262,16 +263,16 @@ export async function getSection(language: string): Promise<JSONData.Section> {
// get TLD for wikipedia according to language group
let urlTLD = "en";
let currentLanguageGroup: JSONData.LanguageGroup | undefined;
try {
currentLanguageGroup = await JSONData.getCurrentGroup(language);
} catch (e) {
const { data: currentLanguageGroup, error } = await tryCatch(
JSONData.getCurrentGroup(language)
);
if (error) {
console.error(
Misc.createErrorMessage(e, "Failed to find current language group")
Misc.createErrorMessage(error, "Failed to find current language group")
);
}
if (currentLanguageGroup !== undefined) {
if (currentLanguageGroup !== null && currentLanguageGroup !== undefined) {
urlTLD = await getTLD(currentLanguageGroup);
}

View file

@ -2,6 +2,7 @@ import { ZodIssue } from "zod";
import { deepClone } from "./misc";
import { isZodError } from "@monkeytype/util/zod";
import * as Notifications from "../elements/notifications";
import { tryCatchSync } from "@monkeytype/util/trycatch";
export class LocalStorageWithSchema<T> {
private key: string;
@ -28,13 +29,13 @@ export class LocalStorageWithSchema<T> {
return this.fallback;
}
let jsonParsed: unknown;
try {
jsonParsed = JSON.parse(value);
} catch (e) {
const { data: jsonParsed, error } = tryCatchSync(
() => JSON.parse(value) as unknown
);
if (error) {
console.log(
`Value from localStorage ${this.key} was not a valid JSON, using fallback`,
e
error
);
window.localStorage.removeItem(this.key);
return this.fallback;

View file

@ -26,6 +26,7 @@ import {
} from "@monkeytype/contracts/schemas/configs";
import { z } from "zod";
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
import { tryCatchSync } from "@monkeytype/util/trycatch";
export async function linkDiscord(hashOverride: string): Promise<void> {
if (!hashOverride) return;
@ -80,15 +81,12 @@ export function loadCustomThemeFromUrl(getOverride?: string): void {
const getValue = Misc.findGetParameter("customTheme", getOverride);
if (getValue === null) return;
let decoded: z.infer<typeof customThemeUrlDataSchema>;
try {
decoded = parseJsonWithSchema(atob(getValue), customThemeUrlDataSchema);
} catch (e) {
console.log("Custom theme URL decoding failed", e);
Notifications.add(
"Failed to load theme from URL: " + (e as Error).message,
0
);
const { data: decoded, error } = tryCatchSync(() =>
parseJsonWithSchema(atob(getValue), customThemeUrlDataSchema)
);
if (error) {
console.log("Custom theme URL decoding failed", error);
Notifications.add("Failed to load theme from URL: " + error.message, 0);
return;
}
@ -156,20 +154,17 @@ const TestSettingsSchema = z.tuple([
z.string().nullable(), //funbox
]);
type SharedTestSettings = z.infer<typeof TestSettingsSchema>;
export function loadTestSettingsFromUrl(getOverride?: string): void {
const getValue = Misc.findGetParameter("testSettings", getOverride);
if (getValue === null) return;
let de: SharedTestSettings;
try {
const decompressed = decompressFromURI(getValue) ?? "";
de = parseJsonWithSchema(decompressed, TestSettingsSchema);
} catch (e) {
console.error("Failed to parse test settings:", e);
const { data: de, error } = tryCatchSync(() =>
parseJsonWithSchema(decompressFromURI(getValue) ?? "", TestSettingsSchema)
);
if (error) {
console.error("Failed to parse test settings:", error);
Notifications.add(
"Failed to load test settings from URL: " + (e as Error).message,
"Failed to load test settings from URL: " + error.message,
0
);
return;

View file

@ -0,0 +1,92 @@
import { describe, it, expect } from "vitest";
import { tryCatch, tryCatchSync } from "../src/trycatch";
describe("tryCatch", () => {
it("should return data on successful promise resolution", async () => {
const result = await tryCatch(Promise.resolve("success"));
expect(result.data).toBe("success");
expect(result.error).toBeNull();
});
it("should return error on promise rejection", async () => {
const testError = new Error("test error");
const result = await tryCatch(Promise.reject(testError));
expect(result.data).toBeNull();
expect(result.error).toBe(testError);
});
it("should handle custom error types", async () => {
class CustomError extends Error {
code: string;
constructor(message: string, code: string) {
super(message);
this.code = code;
}
}
const customError = new CustomError("custom error", "E123");
const result = await tryCatch<string, CustomError>(
Promise.reject(customError)
);
expect(result.data).toBeNull();
expect(result.error).toBe(customError);
expect(result.error?.code).toBe("E123");
});
it("should handle exceptions in async functions", async () => {
const testError = new Error("test error");
const fn = async (): Promise<void> => {
throw testError;
};
const result = await tryCatch(fn());
expect(result.data).toBeNull();
expect(result.error).toBe(testError);
});
});
describe("tryCatchSync", () => {
it("should return data on successful function execution", () => {
const result = tryCatchSync(() => "success");
expect(result.data).toBe("success");
expect(result.error).toBeNull();
});
it("should return error when function throws", () => {
const testError = new Error("test error");
const result = tryCatchSync(() => {
throw testError;
});
expect(result.data).toBeNull();
expect(result.error).toBe(testError);
});
it("should handle complex data structures", () => {
const complexData = {
foo: "bar",
numbers: [1, 2, 3],
nested: { value: true },
};
const result = tryCatchSync(() => complexData);
expect(result.data).toEqual(complexData);
expect(result.error).toBeNull();
});
it("should handle custom error types", () => {
class CustomError extends Error {
code: string;
constructor(message: string, code: string) {
super(message);
this.code = code;
}
}
const customError = new CustomError("custom error", "E123");
const result = tryCatchSync<string, CustomError>(() => {
throw customError;
});
expect(result.data).toBeNull();
expect(result.error).toBe(customError);
expect(result.error?.code).toBe("E123");
});
});

View file

@ -0,0 +1,33 @@
// based on https://gist.github.com/t3dotgg/a486c4ae66d32bf17c09c73609dacc5b
type Success<T> = {
data: T;
error: null;
};
type Failure<E> = {
data: null;
error: E;
};
type Result<T, E = Error> = Success<T> | Failure<E>;
export async function tryCatch<T, E = Error>(
promiseOrFunction: Promise<T>
): Promise<Result<T, E>> {
try {
let data = await promiseOrFunction;
return { data, error: null };
} catch (error) {
return { data: null, error: error as E };
}
}
export function tryCatchSync<T, E = Error>(fn: () => T): Result<T, E> {
try {
let data = fn();
return { data, error: null };
} catch (error) {
return { data: null, error: error as E };
}
}