Bug WikiAsync & Event HandlingUnhandled promise rejection
23 examples

Unhandled promise rejection

Promise rejection not caught or handled.

[ FAQ1 ]

Unhandled promise rejection?

An unhandled promise rejection occurs when a JavaScript promise fails (rejects), but there's no associated .catch() or equivalent error-handling logic to handle the error. Without handling, rejected promises trigger runtime warnings like UnhandledPromiseRejectionWarning, indicating potential stability issues and unexpected behaviors. Ignored promise rejections can mask underlying errors, complicate debugging, and negatively impact the application's reliability and user experience.
[ FAQ2 ]

How to fix unhandled promise rejections

An unhandled promise rejection occurs when a JavaScript promise fails (rejects), but there's no associated .catch() or equivalent error-handling logic to handle the error. Without handling, rejected promises trigger runtime warnings like UnhandledPromiseRejectionWarning, indicating potential stability issues and unexpected behaviors. Ignored promise rejections can mask underlying errors, complicate debugging, and negatively impact the application's reliability and user experience.
diff block
// console.log("Handling invoice.paid:", invoice.id);
- const batchUpdate = [];
- for (const cusProduct of activeCusProducts) {
- // Create invoice
+ InvoiceService.createInvoiceFromStripe({
+ sb,
+ stripeInvoice: invoice,
+ internalCustomerId: activeCusProducts[0].internal_customer_id,
+ productIds: activeCusProducts.map((p) => p.product_id),
+ internalProductIds: activeCusProducts.map((p) => p.internal_product_id),
+ org: org,
+ });
Greptile
greptile
logic: Missing await on async operation - could cause race conditions or unhandled promise rejections
suggested fix
+ await InvoiceService.createInvoiceFromStripe({
sb,
stripeInvoice: invoice,
internalCustomerId: activeCusProducts[0].internal_customer_id,
productIds: activeCusProducts.map((p) => p.product_id),
internalProductIds: activeCusProducts.map((p) => p.internal_product_id),
org: org,
});
diff block
this.pythonProcess = childProcess;
}
+
+ // Handle unzipping if initialDownload is a .zip file
+ if (initialDownload && initialDownload.url.endsWith(".zip")) {
+ const zipFilePath = path.join(initialDownload.save_path, initialDownload.url.split('/').pop()!);
+ const outputDir = initialDownload.save_path;
+ await unzipFile(zipFilePath, outputDir);
Greptile
greptile
style: Missing try/catch block around unzipFile call. Unhandled promise rejection could crash the app.
diff block
+import { Action, ActionPanel, Icon, Color, confirmAlert, Alert, showToast, Toast } from "@raycast/api";
+import AddRuleForm from "./add-rule-form";
+import EditTagsForm from "./tag-library";
+import AddTagForm from "./add-tag-form";
+import { Rule, Tag } from "./types";
+import { getMostRecentProjectPath } from "./utils/vscode-utils";
+import { showFailureToast } from "./utils/utils";
+
+export function AddRuleAction({ onRuleAdded }: { onRuleAdded: (rule: Rule | undefined) => void }) {
+ return (
+ <Action.Push
+ icon={Icon.NewDocument}
+ title="New Rule"
+ target={<AddRuleForm onRuleAdded={onRuleAdded} />}
+ shortcut={{ modifiers: ["cmd"], key: "n" }}
+ />
+ );
+}
+
+export function ApplyRuleAction({
+ rule,
+ onApplyRule,
+}: {
+ rule: Rule;
+ onApplyRule: (rule: Rule, projectPath: string) => Promise<void>;
+}) {
+ return (
+ <Action
+ icon={Icon.CheckCircle}
+ title="Apply Rule"
+ onAction={async () => {
+ const projectPath = await getMostRecentProjectPath();
+ if (!projectPath) {
+ showToast({
+ style: Toast.Style.Failure,
+ title: "Could not determine project path",
+ message: "Please open a recent project in VS Code.",
+ });
+ return;
+ }
+ try {
+ await onApplyRule(rule, projectPath);
+ } catch (error) {
+ showFailureToast("Failed to apply rule", error);
+ }
+ }}
+ />
+ );
+}
+
+export function TogglePinRuleAction({ rule, onTogglePin }: { rule: Rule; onTogglePin: (rule: Rule) => Promise<void> }) {
+ return rule.isPinned ? (
+ <Action
+ icon={Icon.StarDisabled}
+ title="Unpin Rule"
+ onAction={() => onTogglePin(rule)}
+ shortcut={{ modifiers: ["cmd"], key: "i" }}
Greptile
greptile
logic: onTogglePin is async but used without await, could lead to unhandled promise rejection
suggested fix
onAction={async () => {
try {
+ await onTogglePin(rule);
} catch (error) {
+ showFailureToast("Failed to toggle pin", error);
}
}}
shortcut={{ modifiers: ["cmd"], key: "i" }}
diff block
+"use client";
+
+import "@assistant-ui/react-markdown/styles/dot.css";
+
+import {
+ CodeHeaderProps,
+ MarkdownTextPrimitive,
+ unstable_memoizeMarkdownComponents as memoizeMarkdownComponents,
+ useIsMarkdownCodeBlock,
+} from "@assistant-ui/react-markdown";
+import remarkGfm from "remark-gfm";
+import { FC, memo, useState } from "react";
+import { CheckIcon, CopyIcon } from "lucide-react";
+
+import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";
+import { cn } from "@/lib/utils";
+
+const MarkdownTextImpl = () => {
+ return (
+ <MarkdownTextPrimitive
+ remarkPlugins={[remarkGfm]}
+ components={defaultComponents}
+ />
+ );
+};
+
+export const MarkdownText = memo(MarkdownTextImpl);
+
+const CodeHeader: FC<CodeHeaderProps> = ({ language, code }) => {
+ const { isCopied, copyToClipboard } = useCopyToClipboard();
+ const onCopy = () => {
+ if (!code || isCopied) return;
+ copyToClipboard(code);
+ };
+
+ return (
+ <div className="flex items-center justify-between gap-4 rounded-t-lg bg-zinc-900 px-4 py-2 text-sm font-semibold text-white">
+ <span className="lowercase [&>span]:text-xs">{language}</span>
+ <TooltipIconButton tooltip="Copy" onClick={onCopy}>
+ {!isCopied && <CopyIcon />}
+ {isCopied && <CheckIcon />}
+ </TooltipIconButton>
+ </div>
+ );
+};
+
+const useCopyToClipboard = ({
+ copiedDuration = 3000,
+}: {
+ copiedDuration?: number;
+} = {}) => {
+ const [isCopied, setIsCopied] = useState<boolean>(false);
+
+ const copyToClipboard = (value: string) => {
+ if (!value) return;
+
+ navigator.clipboard.writeText(value).then(() => {
+ setIsCopied(true);
+ setTimeout(() => setIsCopied(false), copiedDuration);
+ });
+ };
Greptile
greptile
logic: navigator.clipboard.writeText() can fail - should handle the promise rejection case to prevent unhandled promise rejections
suggested fix
navigator.clipboard.writeText(value).then(() => {
setIsCopied(true);
setTimeout(() => setIsCopied(false), copiedDuration);
+ }).catch((error) => {
+ console.error('Failed to copy text:', error);
});
};
diff block
);
}, [profileDailyPlans]);
- useEffect(() => {
- if (firstLoad) {
- getMyDailyPlans();
- getAllDayPlans();
- getEmployeeDayPlans(user?.employee.id || '');
- }
- }, [getMyDailyPlans, activeTeam?.id, getAllDayPlans, firstLoad, getEmployeeDayPlans, user?.employee.id]);
+ const handleFirstLoad = useCallback(async () => {
+ await loadAllDayPlans();
+ await loadMyDailyPlans();
+ await loadEmployeeDayPlans();
+ firstLoadDailyPlanData();
+ }, [firstLoadDailyPlanData, loadAllDayPlans, loadEmployeeDayPlans, loadMyDailyPlans]);
Greptile
greptile
style: handleFirstLoad should catch errors from the load functions to prevent unhandled promise rejections
suggested fix
const handleFirstLoad = useCallback(async () => {
+ try {
await loadAllDayPlans();
await loadMyDailyPlans();
await loadEmployeeDayPlans();
firstLoadDailyPlanData();
+ } catch (error) {
+ console.error('Error during first load:', error);
+ }
}, [firstLoadDailyPlanData, loadAllDayPlans, loadEmployeeDayPlans, loadMyDailyPlans]);
diff block
error: errorMessage,
},
}))
- } finally {
- setIsDownloading(false)
+ throw e
Greptile
greptile
logic: Re-throwing error after setting error state may cause unhandled promise rejection ```suggestion + toast.error(`Failed to download model: ${errorMessage}`) ```
diff block
const messageStream = AssistantMessageStream.fromAssistantStream(stream);
for await (const result of messageStream) {
Greptile
greptile
style: Consider handling potential errors from AssistantMessageStream.fromAssistantStream() to prevent unhandled promise rejections
suggested fix
+ try {
+ const messageStream = AssistantMessageStream.fromAssistantStream(stream);
+ for await (const result of messageStream) {
diff block
+import { Link } from '@posthog/lemon-ui'
+import { CodeSnippet, Language } from 'lib/components/CodeSnippet'
+
+import { SDKInstallNodeInstructions } from '../sdk-install-instructions'
+import { SDKInstallSvelteJSInstructions } from '../sdk-install-instructions/svelte'
+import { JSManualCapture, NodeManualCapture } from './FinalSteps'
+
+export function SvelteInstructions(): JSX.Element {
+ return (
+ <>
+ <p>
+ PostHog exception autocapture relies on{' '}
+ <Link to="https://svelte.dev/docs/kit/hooks" target="_blank">
+ SvelteKit Hooks
+ </Link>{' '}
+ to capture exceptions in the client and server-side.
+ </p>
+ <h2>Client-side</h2>
+ <SDKInstallSvelteJSInstructions />
+ <p>You will need to capture exceptions in the handleError callback in your client-side hooks file.</p>
+ <CodeSnippet language={Language.JavaScript}>{clientSideHooks}</CodeSnippet>
+ <JSManualCapture />
+ <h2>Server-side</h2>
+ <SDKInstallNodeInstructions />
+ <p>
+ To capture exceptions on the server-side, you will also need to implement the <code>handleError</code>{' '}
+ callback
+ </p>
+ <CodeSnippet language={Language.JavaScript}>{serverSideHooks}</CodeSnippet>
+ <NodeManualCapture />
+ </>
+ )
+}
+
+const clientSideHooks = `// src/hooks.client.js
+
+import { captureException } from 'posthog-js';
+import type { HandleClientError } from '@sveltejs/kit';
+
+export const handleError = ({ error, status }: HandleClientError) => {
+ // SvelteKit 2.0 offers a reliable way to check for a 404 error:
+ if (status !== 404) {
+ captureException(error);
+ }
+};
+`
+
+const serverSideHooks = `// src/hooks.server.js
+
+import type { HandleServerError } from '@sveltejs/kit';
+import { captureException, shutdown } from 'posthog-node';
+
+export const handleError = async ({ error, status }: HandleServerError) => {
+ if (status !== 404) {
+ captureException(error);
+
+ await shutdown();
+ }
Greptile
greptile
logic: shutdown() should be in a try/catch block to prevent potential unhandled promise rejections
suggested fix
export const handleError = async ({ error, status }: HandleServerError) => {
if (status !== 404) {
captureException(error);
+ try {
await shutdown();
+ } catch (shutdownError) {
+ console.error('Failed to shutdown PostHog client:', shutdownError);
}
}
}
diff block
+const { loadEnvConfig } = require("@next/env");
+const git = require("isomorphic-git")
+const http = require("isomorphic-git/http/node")
+const fs = require("fs")
+const path = require("path")
+const os = require('os')
+
+loadEnvConfig(process.cwd());
+
+const token = process.env.GH_TOKEN;
+const url = process.env.DATA_REPOSITORY;
+
+if (!url) {
+ throw new Error("'DATA_REPOSITORY' must be defined as environment variable.");
+}
+
+if (!token) {
+ throw new Error("'GH_TOKEN' must be defined as environment variable.");
+}
+
+
+function getContentPath() {
+ const contentDir = '.content';
+ if (process.env.VERCEL) {
+ return path.join(os.tmpdir(), contentDir);
+ }
+
+ return path.join(process.cwd(), contentDir);
+}
+
+const auth = { username: "x-access-token", password: token };
+const dest = getContentPath();
+
+async function main() {
+ await fs.promises.mkdir(dest, { recursive: true });
+
+ await git.clone({
+ onAuth: () => auth,
+ fs,
+ http,
+ url,
+ dir: dest,
+ singleBranch: true,
+ });
+}
+
+main()
Greptile
greptile
logic: Missing error handling for main(). Unhandled promise rejection could crash the process.
suggested fix
+main().catch(err => {
+ console.error('Failed to clone repository:', err);
+ process.exit(1);
});
diff block
sentToDql = true
): Promise<ReturnType<Step>> {
const timer = new Date()
- return runInSpan(
- {
- op: 'runStep',
- description: step.name,
- },
- async () => {
- const sendToSentry = false
- const timeout = timeoutGuard(
- `Event pipeline step stalled. Timeout warning after ${this.hub.PIPELINE_STEP_STALLED_LOG_TIMEOUT} sec! step=${step.name} team_id=${teamId} distinct_id=${this.originalEvent.distinct_id}`,
- () => ({
- step: step.name,
- event: JSON.stringify(this.originalEvent),
- teamId: teamId,
- distinctId: this.originalEvent.distinct_id,
- }),
- this.hub.PIPELINE_STEP_STALLED_LOG_TIMEOUT * 1000,
- sendToSentry
- )
- try {
- const result = await step(...args)
- pipelineStepMsSummary.labels(step.name).observe(Date.now() - timer.getTime())
- return result
- } catch (err) {
- await this.handleError(err, step.name, args, teamId, sentToDql)
- } finally {
- clearTimeout(timeout)
- }
+
+ return new Promise(async () => {
Greptile
greptile
logic: new Promise(async () =&gt; {}) is an anti-pattern that can lead to unhandled promise rejections. Should be: return new Promise((resolve, reject) =&gt; {
suggested fix
+ return new Promise((resolve, reject) => {
diff block
-import { INextParams } from '@app/interfaces';
import { authenticatedGuard } from '@app/services/server/guards/authenticated-guard-app';
import { editOrganizationProjectsSettingsRequest } from '@app/services/server/requests';
import { NextResponse } from 'next/server';
-export async function PUT(req: Request, { params }: INextParams) {
+export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
const res = new NextResponse();
-
- if (!params.id) {
+ const id = (await params).id;
+ if (!id) {
return;
}
Greptile
greptile
logic: Missing error handling if params Promise rejects - could cause unhandled promise rejection
diff block
: req.accepts('json')
? 'json'
: 'text'),
- req.getUser(),
+ await req.getUser(),
Greptile
greptile
style: Consider adding error handling around `req.getUser()` in case it fails, to prevent unhandled promise rejections.
suggested fix
+ await req.getUser().catch(err => {
+ console.error('[error] Failed to get user:', err);
+ return null;
+ }),
diff block
if (user.iBy) {
const inviteSender = {
id: user.iBy,
- name: request.getUserNameFromId(user.iBy),
+ name: await userModel.fetchUserNameById(user.iBy),
};
follow(storedUser, inviteSender, 'invite');
follow(inviteSender, storedUser, 'invite');
notifModel.inviteAccepted(user.iBy, storedUser);
}
- loginAndRedirectTo(onboardingUrl || '/');
+ await loginAndRedirectTo(onboardingUrl || '/');
}
if (user.iBy || checkInvites)
inviteController.checkInviteCode(request, user, response, function () {
- registerUser();
+ registerUser(); // no await because we're in a (sync) callback
Greptile
greptile
logic: registerUser() is async but called without await in a callback - this could lead to unhandled promise rejections
diff block
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTitle
+} from '@/core/components/ui/alert-dialog';
+import { Button, Card, Modal, Text } from '@/core/components';
+import { ReloadIcon } from '@radix-ui/react-icons';
+
+interface AlertDialogConfirmationProps {
+ title: string;
+ description: string;
+ confirmText?: string;
+ cancelText?: string;
+ onConfirm: () => void;
+ onCancel: () => void;
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+ loading?: boolean;
+}
+
+export function AlertDialogConfirmation({
+ title,
+ description,
+ confirmText = 'Continue',
+ cancelText = 'Cancel',
+ onConfirm,
+ onCancel,
+ isOpen,
+ onOpenChange,
+ loading
+}: AlertDialogConfirmationProps) {
+ return (
+ <AlertDialog open={isOpen} onOpenChange={onOpenChange} defaultOpen={false}>
+ <AlertDialogContent>
+ <AlertDialogHeader>
+ <AlertDialogTitle>{title}</AlertDialogTitle>
+ <AlertDialogDescription>{description}</AlertDialogDescription>
+ </AlertDialogHeader>
+ <AlertDialogFooter>
+ <AlertDialogCancel onClick={onCancel} autoFocus>
+ {cancelText}
+ </AlertDialogCancel>
+ <AlertDialogAction
+ className="px-4 py-2 text-sm hover:bg-red-600 hover:text-white font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light"
+ aria-label={loading ? 'Confirming action...' : confirmText}
+ disabled={loading}
+ onClick={onConfirm}
+ >
+ {loading && <ReloadIcon className="w-4 h-4 mr-2 animate-spin" />}
+ {loading ? 'Processing...' : confirmText}
+ </AlertDialogAction>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialog>
+ );
+}
+
+export const AlertConfirmationModal = ({
+ open,
+ close,
+ title,
+ description,
+ onAction,
+ loading,
+ confirmText = 'Continue',
+ cancelText = 'Cancel',
+ countID = 0
+}: {
+ open: boolean;
+ close: () => void;
+ onAction: () => any;
+ title: string;
+ description: string;
+ loading: boolean;
+ confirmText?: string;
+ cancelText?: string;
+ countID?: number;
+}) => {
+ return (
+ <>
+ <Modal isOpen={open} closeModal={close} showCloseIcon={false}>
+ <Card className="w-full md:min-w-[480px]" shadow="custom">
+ <div className="flex flex-col items-center justify-between">
+ <div className="flex flex-col">
+ <Text.Heading as="h3" className="text-2xl text-center">
+ {title}
+ </Text.Heading>
+ <div className="flex items-center gap-x-2">
+ <span className="text-center">{description}</span>
+ {countID > 0 && (
+ <span className=" h-7 w-7 flex items-center justify-center text-center text-xl rounded-full font-bold bg-primary dark:bg-primary-light text-white">
+ {' '}
+ {countID}
+ </span>
+ )}
+ </div>
+ </div>
+ <div className="flex items-center justify-end gap-x-5 w-full mt-4">
+ <Button
+ type="button"
+ onClick={close}
+ className={
+ 'px-4 py-2 text-sm font-medium text-gray-700 dark:text-red-600 border rounded-md bg-light--theme-light dark:!bg-dark--theme-light'
+ }
+ >
+ {cancelText}
+ </Button>
+ <Button
+ variant="danger"
+ type="submit"
+ className="px-4 py-2 text-sm hover:bg-red-600 hover:text-white font-medium text-red-600 border border-red-600 rounded-md bg-light--theme-light dark:!bg-dark--theme-light"
+ disabled={loading}
+ loading={loading}
+ onClick={() => {
+ onAction()?.then(() => {
+ close();
+ });
+ }}
Greptile
greptile
style: potential unhandled promise rejection if onAction() fails
diff block
})
}
- private async consumeBatch(invocations: HogFunctionInvocation[], source: CyclotronJobQueueKind) {
+ private async consumeBatch(
+ invocations: HogFunctionInvocation[],
+ source: CyclotronJobQueueKind
+ ): Promise<{ backgroundTask: Promise<any> }> {
cyclotronBatchUtilizationGauge
.labels({ queue: this.queue, source })
.set(invocations.length / this.config.CDP_CYCLOTRON_BATCH_SIZE)
- await this._consumeBatch!(invocations)
+ const result = await this._consumeBatch!(invocations)
counterJobsProcessed.inc({ queue: this.queue, source }, invocations.length)
Greptile
greptile
logic: No error handling around _consumeBatch call - could lead to unhandled promise rejections
diff block
refreshParam = refresh || 'blocking'
}
- if ((refreshParam === 'blocking' || refreshParam === 'force_blocking') && setPollResponse && queryId) {
- const currentTeamId = teamLogic.findMounted()?.values.currentTeamId
- const timeoutId = setTimeout(() => {
- void api.stream(`/api/environments/${currentTeamId}/query/${queryId}/progress/`, {
- onMessage: (event) => {
- try {
- const progressData = JSON.parse(event.data)
- if (progressData.error) {
- console.warn('Failed to poll query progress:', progressData.error)
- return
+ if (useOptimizedPolling) {
+ return new Promise((resolve, reject) => {
+ const abortController = new AbortController()
+
+ void api
+ .stream(`/api/environments/${currentTeamId}/query_awaited/`, {
+ method: 'POST',
+ data: {
+ query: queryNode,
+ client_query_id: queryId,
+ refresh: refreshParam,
+ filters_override: filtersOverride,
+ variables_override: variablesOverride,
+ },
+ signal: abortController.signal,
+ onMessage(ev) {
+ try {
+ const data = JSON.parse(ev.data)
+ if (data.error) {
+ logQueryEvent('error', data, queryNode)
+ abortController.abort()
+ // Create an error object that matches the API error format
+ const error = {
+ message: data.error,
+ status: data.status_code || 500,
+ detail: data.error_message || data.error,
+ type: 'network_error',
+ }
+ reject(error)
+ } else if (data.complete === false) {
+ // Progress event - no results yet
+ logQueryEvent('progress', data, queryNode)
+ if (setPollResponse) {
+ setPollResponse(data)
+ }
+ } else {
+ // Final results
+ logQueryEvent('data', data, queryNode)
+ abortController.abort()
+ resolve(data)
+ }
+ } catch (e) {
+ abortController.abort()
+ reject(e)
}
- setPollResponse({
- id: queryId || '',
- team_id: 0,
- error: false,
- complete: false,
- query_async: true,
- error_message: null,
- query_progress: progressData,
- })
- } catch (e) {
- console.warn('Failed to parse progress data:', e)
- }
- },
- onError: (error) => {
- console.warn('Failed to poll query progress:', error)
- },
- signal: methodOptions?.signal,
- })
- }, 500) // Give the query time to start
- methodOptions?.signal?.addEventListener('abort', () => {
- clearTimeout(timeoutId)
+ },
+ onError(err) {
+ abortController.abort()
+ reject(err)
+ throw err // make sure fetchEventSource doesn't attempt to retry
+ },
Greptile
greptile
logic: Throwing error after reject() is redundant and could cause unhandled promise rejection
suggested fix
abortController.abort()
reject(err)
+ return // make sure fetchEventSource doesn't attempt to retry
},
diff block
+import { LocalStorage } from "@raycast/api";
+import { showFailureToast } from "@raycast/utils";
+import { useState, useEffect } from "react";
+import { Resolution } from "../types";
+import { isSameResolution } from "../utils/resolution";
+
+export function useStarredResolutions() {
+ const [starredResolutions, setStarredResolutions] = useState<Resolution[]>([]);
+ const [refreshTrigger, setRefreshTrigger] = useState(0);
+ const [isLoading, setIsLoading] = useState(true);
+
+ // Load starred resolutions
+ useEffect(() => {
+ async function loadStarredResolutions() {
+ try {
+ const storedResolutions = await LocalStorage.getItem<string>("starred-resolutions");
+ if (storedResolutions) {
+ const parsedResolutions = JSON.parse(storedResolutions);
+ // Add isStarred=true to all starred resolutions
+ setStarredResolutions(
+ parsedResolutions.map((resolution: Resolution) => ({
+ ...resolution,
+ isStarred: true,
+ })),
+ );
+ }
+ } catch (error) {
+ console.error("Error loading starred resolutions:", error);
+ await showFailureToast({
+ title: "Failed to load starred resolutions",
+ message: error instanceof Error ? error.message : "Unknown error occurred",
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ loadStarredResolutions();
+ }, [refreshTrigger]);
+
+ // Check if a resolution is starred
+ async function isResolutionStarred(resolution: Resolution): Promise<boolean> {
+ try {
+ const storedResolutions = await LocalStorage.getItem<string>("starred-resolutions");
+ if (!storedResolutions) return false;
+
+ const parsedResolutions = JSON.parse(storedResolutions);
+ return parsedResolutions.some((r: Resolution) => isSameResolution(r, resolution));
+ } catch (error) {
+ console.error("Error checking if resolution is starred:", error);
+ await showFailureToast({
+ title: "Failed to check starred status",
+ message: error instanceof Error ? error.message : "Unknown error occurred",
+ });
+ return false;
+ }
+ }
+
+ // Remove a resolution from starred list
+ async function removeStarredResolution(resolution: Resolution): Promise<void> {
+ try {
+ const storedResolutions = await LocalStorage.getItem<string>("starred-resolutions");
+ if (!storedResolutions) return;
+
+ const parsedResolutions = JSON.parse(storedResolutions);
+ const updatedResolutions = parsedResolutions.filter((r: Resolution) => !isSameResolution(r, resolution));
+
+ // First ensure storage update succeeds
+ await LocalStorage.setItem("starred-resolutions", JSON.stringify(updatedResolutions));
+
+ // Only refresh after storage is confirmed
+ refreshStarredResolutions();
+ } catch (error) {
+ console.error("Error removing starred resolution:", error);
+ await showFailureToast({
+ title: "Failed to remove starred resolution",
+ message: error instanceof Error ? error.message : "Unknown error occurred",
+ });
+ }
+ }
+
+ // Function to toggle star status
+ async function toggleStarResolution(resolution: Resolution) {
+ try {
+ const isStarred = starredResolutions.some((r) => isSameResolution(r, resolution));
+ const updatedResolutions = isStarred
+ ? starredResolutions.filter((r) => !isSameResolution(r, resolution))
+ : [...starredResolutions, { ...resolution, isStarred: true }];
+
+ // First ensure storage update succeeds
+ await LocalStorage.setItem("starred-resolutions", JSON.stringify(updatedResolutions));
+
+ // Only update state after storage is confirmed
+ setStarredResolutions(updatedResolutions);
+ } catch (error) {
+ console.error("Error toggling star status:", error);
+ await showFailureToast({
+ title: "Failed to toggle star status",
+ message: error instanceof Error ? error.message : "Unknown error occurred",
+ });
+ throw error;
+ }
Greptile
greptile
logic: Error is thrown after showing toast, which may lead to unhandled promise rejection in parent components
diff block
+import { handleApiRequest } from "@/route-handlers/smart-route-handler";
+import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env";
+import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
+import { createNodeHttpServerDuplex } from "@stackframe/stack-shared/dist/utils/node-http";
+import { NextRequest, NextResponse } from "next/server";
+import { createOidcProvider } from "../../../../idp";
+
+export const dynamic = "force-dynamic";
+
+const pathPrefix = "/api/v1/integrations/custom/oauth/idp";
+
+// we want to initialize the OIDC provider lazily so it's not initiated at build time
+let _oidcCallbackPromiseCache: Promise<any> | undefined;
+function getOidcCallbackPromise() {
+ if (!_oidcCallbackPromiseCache) {
+ const apiBaseUrl = new URL(getEnvVariable("NEXT_PUBLIC_STACK_API_URL"));
+ const idpBaseUrl = new URL(pathPrefix, apiBaseUrl);
+ _oidcCallbackPromiseCache = (async () => {
+ const oidc = await createOidcProvider({
+ id: "stack-preconfigured-idp:integrations/custom",
+ baseUrl: idpBaseUrl.toString(),
+ clientInteractionUrl: new URL(`/integrations/custom/confirm`, getEnvVariable("NEXT_PUBLIC_STACK_DASHBOARD_URL")).toString(),
+ });
+ return oidc.callback();
Greptile
greptile
logic: No error handling for createOidcProvider failure. Could cause unhandled promise rejection. ```suggestion + try { const oidc = await createOidcProvider({ id: "stack-preconfigured-idp:integrations/custom", baseUrl: idpBaseUrl.toString(), clientInteractionUrl: new URL(`/integrations/custom/confirm`, getEnvVariable("NEXT_PUBLIC_STACK_DASHBOARD_URL")).toString(), }); return oidc.callback(); + } catch (error) { + console.error('Failed to create OIDC provider:', error); + throw error; + } ```
diff block
+import type { BusterSocketResponseRoute, BusterSocketRequest } from '@/api/buster_socket';
+import type { QueryKey, UseQueryOptions } from '@tanstack/react-query';
+import type { InferBusterSocketResponseData } from './types';
+import { useBusterWebSocket } from '@/context/BusterWebSocket';
+import { useEffect } from 'react';
+import { useMemoizedFn } from 'ahooks';
+import { useSocketQueryOn } from './useSocketQueryOn';
+
+export const useSocketQueryEmitOn = <
+ TRoute extends BusterSocketResponseRoute,
+ TError = unknown,
+ TData = InferBusterSocketResponseData<TRoute>,
+ TQueryKey extends QueryKey = QueryKey
+>(
+ socketRequest: BusterSocketRequest,
+ socketResponse: TRoute,
+ options: UseQueryOptions<TData, TError, TData, TQueryKey>,
+ callback?:
+ | ((currentData: TData | null, newData: InferBusterSocketResponseData<TRoute>) => TData)
+ | null,
+ enabledTriggerProp?: boolean | string
+) => {
+ const busterSocket = useBusterWebSocket();
+ const enabledTrigger = enabledTriggerProp ?? true;
+
+ const emitQueryFn = useMemoizedFn(async () => {
+ busterSocket.emit(socketRequest);
+ });
Greptile
greptile
logic: emitQueryFn is marked async but doesn't await anything, which could lead to unhandled promise rejections if socket.emit fails
suggested fix
+ const emitQueryFn = useMemoizedFn(() => {
busterSocket.emit(socketRequest);
});
diff block
+/* eslint-disable no-mixed-spaces-and-tabs */
+import { useIssueType } from '@app/hooks';
+import { IIcon, IIssueTypesItemList } from '@app/interfaces';
+import { userState } from '@app/stores';
+import { Spinner } from '@/core/components/ui/loaders/spinner';
+import { PlusIcon } from '@heroicons/react/20/solid';
+import { Button, ColorPicker, InputField, Text } from '@/core/components';
+import { useCallback, useEffect, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { useTranslations } from 'next-intl';
+import { useAtom } from 'jotai';
+import { generateIconList } from './icon-items';
+import IconPopover from './icon-popover';
+import { StatusesListCard } from './list-card';
+
+export const IssueTypesForm = () => {
+ const t = useTranslations();
+ const [user] = useAtom(userState);
+ const { register, setValue, handleSubmit, reset } = useForm();
+ const [createNew, setCreateNew] = useState(false);
+ const [edit, setEdit] = useState<IIssueTypesItemList | null>(null);
+
+ const taskStatusIconList: IIcon[] = generateIconList('task-statuses', [
+ 'open',
+ 'in-progress',
+ 'ready',
+ 'in-review',
+ 'blocked',
+ 'completed'
+ ]);
+ const taskSizesIconList: IIcon[] = generateIconList('task-sizes', ['x-large', 'large', 'medium', 'small', 'tiny']);
+ const taskPrioritiesIconList: IIcon[] = generateIconList('task-priorities', ['urgent', 'high', 'medium', 'low']);
+
+ const iconList: IIcon[] = [...taskStatusIconList, ...taskSizesIconList, ...taskPrioritiesIconList];
+
+ const {
+ loading,
+ issueTypes,
+ createIssueType,
+ deleteIssueType,
+ editIssueType,
+ createIssueTypeLoading,
+ editIssueTypeLoading
+ } = useIssueType();
+
+ useEffect(() => {
+ if (!edit) {
+ setValue('name', '');
+ setValue('color', '');
+ setValue('icon', '');
+ }
+ }, [issueTypes, edit, setValue]);
+
+ useEffect(() => {
+ if (edit) {
+ setValue('name', edit.name);
+ setValue('color', edit.color);
+ setValue('icon', edit.icon);
+ } else {
+ setValue('name', '');
+ setValue('color', '');
+ setValue('icon', '');
+ }
+ }, [edit, setValue, createIssueType, editIssueType, user?.employee?.organizationId, user?.tenantId]);
+
+ const onSubmit = useCallback(
+ async (values: any) => {
+ if (createNew) {
+ createIssueType({
+ name: values.name,
+ color: values.color,
+ // description: '',
+ organizationId: user?.employee?.organizationId,
+ tenantId: user?.tenantId,
+ icon: values.icon
+ // projectId: '',
+ })?.then(() => {
Greptile
greptile
logic: Optional chaining on createIssueType could result in unhandled promise rejection if function returns undefined
diff block
}
}
+ async rename(imageName: string, newName: string): Promise<void> {
+ const projectFolder = this.projectsManager.project?.folderPath;
+ if (!projectFolder) {
+ console.error('Failed to rename image, projectFolder not found');
+ return;
+ }
+ await invokeMainChannel<string, string>(
+ MainChannels.RENAME_IMAGE_IN_PROJECT,
+ projectFolder,
+ imageName,
+ newName,
+ );
+ this.scanImages();
+ }
Greptile
greptile
logic: The rename method lacks error handling try/catch block unlike delete and upload methods. This could cause unhandled promise rejections.
suggested fix
async rename(imageName: string, newName: string): Promise<void> {
+ try {
const projectFolder = this.projectsManager.project?.folderPath;
if (!projectFolder) {
console.error('Failed to rename image, projectFolder not found');
return;
}
await invokeMainChannel<string, string>(
MainChannels.RENAME_IMAGE_IN_PROJECT,
projectFolder,
imageName,
newName,
);
this.scanImages();
+ } catch (error) {
+ console.error('Error renaming image:', error);
+ throw error;
}
}
diff block
+import axios from "axios";
+import { getPreferenceValues } from "@raycast/api";
+
+interface Preferences {
+ apiKey: string;
+ aiApiBaseUrl: string;
+ model: string;
+}
+
+interface Message {
+ role: "user" | "assistant" | "system";
+ content: string;
+}
+
+export interface InkeepLink {
+ label?: string | null;
+ url: string;
+ title?: string | null;
+ description?: string | null;
+ type?: string | null;
+ breadcrumbs?: string[] | null;
+}
+
+export interface AIAnnotations {
+ answerConfidence?: "very_confident" | "somewhat_confident" | "not_confident" | "no_sources" | "other" | string | null;
+}
+
+export interface ToolCall {
+ id: string;
+ type: string;
+ function: {
+ name: string;
+ arguments: string;
+ };
+}
+
+export interface InkeepToolResponse {
+ links?: InkeepLink[] | null;
+ aiAnnotations?: AIAnnotations;
+}
+
+// This interface is used for type checking when building the request
+export interface InkeepCompletionRequest {
+ model: string;
+ messages: Message[];
+ stream?: boolean;
+ tools?: Array<{
+ type: string;
+ function: {
+ name: string;
+ description?: string;
+ parameters?: Record<string, unknown>;
+ };
+ }>;
+ tool_choice?: string | { type: string; function: { name: string } };
+}
+
+interface InkeepCompletionResponse {
+ id: string;
+ object: string;
+ created: number;
+ model: string;
+ choices: {
+ index: number;
+ message: {
+ role: string;
+ content: string;
+ tool_calls?: ToolCall[];
+ };
+ finish_reason: string;
+ }[];
+ usage: {
+ prompt_tokens: number;
+ completion_tokens: number;
+ total_tokens: number;
+ };
+}
+
+interface StreamChunk {
+ id: string;
+ object: string;
+ created: number;
+ model: string;
+ choices: {
+ index: number;
+ delta: {
+ content?: string;
+ role?: string;
+ tool_calls?: Partial<ToolCall>[];
+ };
+ finish_reason: string | null;
+ }[];
+}
+
+export interface InkeepResponse {
+ content: string;
+ links?: InkeepLink[];
+ aiAnnotations?: AIAnnotations;
+}
+
+export async function getInkeepCompletion(prompt: string): Promise<InkeepResponse> {
+ const preferences = getPreferenceValues<Preferences>();
+ const apiBaseUrl = preferences.aiApiBaseUrl.endsWith("/")
+ ? preferences.aiApiBaseUrl.slice(0, -1)
+ : preferences.aiApiBaseUrl;
+
+ try {
+ const response = await axios.post<InkeepCompletionResponse>(
+ `${apiBaseUrl}/chat/completions`,
+ {
+ model: preferences.model,
+ messages: [
+ {
+ role: "user",
+ content: prompt,
+ },
+ ],
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "provideLinks",
+ description: "Provides a list of links (sources) used by the AI assistant to generate a response",
+ },
+ },
+ {
+ type: "function",
+ function: {
+ name: "provideAIAnnotations",
+ description: "Provides labels for the response, like answerConfidence",
+ parameters: {
+ type: "object",
+ properties: {
+ aiAnnotations: {
+ type: "object",
+ properties: {
+ answerConfidence: {
+ type: "string",
+ enum: ["very_confident", "somewhat_confident", "not_confident", "no_sources", "other"],
+ description: "The confidence level of the answer",
+ },
+ },
+ required: ["answerConfidence"],
+ },
+ },
+ required: ["aiAnnotations"],
+ },
+ },
+ },
+ ],
+ },
+ {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${preferences.apiKey}`,
+ },
+ },
+ );
+
+ const message = response.data.choices[0].message;
+ const result: InkeepResponse = {
+ content: message.content,
+ };
+
+ // Process tool calls if any
+ if (message.tool_calls && message.tool_calls.length > 0) {
+ for (const toolCall of message.tool_calls) {
+ if (toolCall.function.name === "provideLinks") {
+ const toolResponse = JSON.parse(toolCall.function.arguments) as { links?: InkeepLink[] };
+ result.links = toolResponse.links || [];
+ } else if (toolCall.function.name === "provideAIAnnotations") {
+ const toolResponse = JSON.parse(toolCall.function.arguments) as { aiAnnotations?: AIAnnotations };
+ result.aiAnnotations = toolResponse.aiAnnotations;
+ }
+ }
+ }
+
+ return result;
+ } catch (error: unknown) {
+ if (axios.isAxiosError(error) && error.response) {
+ console.error("API Error:", error.response.data);
+ throw new Error(`Inkeep API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
+ }
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ throw new Error(`Error fetching completion: ${errorMessage}`);
+ }
+}
+
+export async function streamInkeepCompletion(
+ prompt: string,
+ onChunk: (chunk: string) => void,
+ onToolCall: (toolName: string, args: string) => void,
+ onComplete: (fullResponse: InkeepResponse) => void,
+): Promise<void> {
+ const preferences = getPreferenceValues<Preferences>();
+ const apiBaseUrl = preferences.aiApiBaseUrl.endsWith("/")
+ ? preferences.aiApiBaseUrl.slice(0, -1)
+ : preferences.aiApiBaseUrl;
+ let fullContent = "";
+ const toolCalls: ToolCall[] = [];
+ let partialToolCalls: Partial<ToolCall>[] = [];
+
+ try {
+ const response = await axios.post(
+ `${apiBaseUrl}/chat/completions`,
+ {
+ model: preferences.model,
+ messages: [
+ {
+ role: "user",
+ content: prompt,
+ },
+ ],
+ stream: true,
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "provideLinks",
+ description: "Provides a list of links (sources) used by the AI assistant to generate a response",
+ },
+ },
+ {
+ type: "function",
+ function: {
+ name: "provideAIAnnotations",
+ description: "Provides labels for the response, like answerConfidence",
+ parameters: {
+ type: "object",
+ properties: {
+ aiAnnotations: {
+ type: "object",
+ properties: {
+ answerConfidence: {
+ type: "string",
+ enum: ["very_confident", "somewhat_confident", "not_confident", "no_sources", "other"],
+ description: "The confidence level of the answer",
+ },
+ },
+ required: ["answerConfidence"],
+ },
+ },
+ required: ["aiAnnotations"],
+ },
+ },
+ },
+ ],
+ },
+ {
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${preferences.apiKey}`,
+ },
+ responseType: "stream",
+ },
+ );
+
+ const stream = response.data;
+ let buffer = "";
+
+ stream.on("data", (chunk: Buffer) => {
+ const chunkStr = chunk.toString();
+ buffer += chunkStr;
+
+ // Process complete lines
+ const lines = buffer.split("\n");
+ buffer = lines.pop() || ""; // Keep the last incomplete line in the buffer
+
+ for (const line of lines) {
+ if (line.trim() === "") continue;
+ if (line.trim() === "data: [DONE]") continue;
+
+ try {
+ // Remove the "data: " prefix
+ const jsonStr = line.replace(/^data: /, "").trim();
+ const data = JSON.parse(jsonStr) as StreamChunk;
+
+ if (data.choices && data.choices.length > 0) {
+ const choice = data.choices[0];
+
+ // Handle content updates
+ if (choice.delta.content) {
+ const content = choice.delta.content;
+ fullContent += content;
+ onChunk(content);
+ }
+
+ // Handle tool calls
+ if (choice.delta.tool_calls) {
+ // Merge partial tool calls
+ if (!partialToolCalls.length && choice.delta.tool_calls.length) {
+ partialToolCalls = choice.delta.tool_calls.map((tc) => ({ ...tc }));
+ } else {
+ choice.delta.tool_calls.forEach((deltaToolCall, index) => {
+ if (!partialToolCalls[index]) {
+ partialToolCalls[index] = { ...deltaToolCall };
+ } else {
+ // Merge function properties
+ if (deltaToolCall.function) {
+ if (!partialToolCalls[index].function) {
+ partialToolCalls[index].function = { ...deltaToolCall.function };
+ } else {
+ if (deltaToolCall.function.name) {
+ partialToolCalls[index].function!.name = deltaToolCall.function.name;
+ }
+ if (deltaToolCall.function.arguments) {
+ partialToolCalls[index].function!.arguments =
+ (partialToolCalls[index].function!.arguments || "") + deltaToolCall.function.arguments;
+ }
+ }
+ }
+
+ // Merge other properties
+ if (deltaToolCall.id) {
+ partialToolCalls[index].id = deltaToolCall.id;
+ }
+ if (deltaToolCall.type) {
+ partialToolCalls[index].type = deltaToolCall.type;
+ }
+ }
+ });
+ }
+
+ // Check if any tool call is complete and can be processed
+ for (let i = partialToolCalls.length - 1; i >= 0; i--) {
+ const tc = partialToolCalls[i];
+ if (tc.id && tc.type && tc.function?.name && tc.function?.arguments) {
+ try {
+ // Validate JSON before processing
+ const args = tc.function.arguments;
+ // Check if the JSON is valid and complete
+ if (isValidJSON(args)) {
+ const completeToolCall = tc as ToolCall;
+ toolCalls.push(completeToolCall);
+ onToolCall(completeToolCall.function.name, completeToolCall.function.arguments);
+ // Remove from partial list
+ partialToolCalls.splice(i, 1);
+ }
+ } catch (e) {
+ console.error("Error validating tool call arguments:", e);
+ // Keep in partial list to collect more chunks
+ }
+ }
+ }
+ }
+ }
+ } catch (e) {
+ console.error("Error parsing stream chunk:", e, line);
+ }
+ }
+ });
+
+ stream.on("end", () => {
+ // Process any remaining tool calls
+ for (let i = partialToolCalls.length - 1; i >= 0; i--) {
+ const tc = partialToolCalls[i];
+ if (tc.id && tc.type && tc.function?.name && tc.function?.arguments) {
+ try {
+ // Validate JSON before processing
+ const args = tc.function.arguments;
+ // Check if the JSON is valid and complete
+ if (isValidJSON(args)) {
+ const completeToolCall = tc as ToolCall;
+ toolCalls.push(completeToolCall);
+ onToolCall(completeToolCall.function.name, completeToolCall.function.arguments);
+ } else {
+ console.warn("Incomplete JSON in tool call arguments:", args);
+ }
+ } catch (e) {
+ console.error("Error processing remaining tool call:", e);
+ }
+ }
+ }
+
+ // Create final response
+ const result: InkeepResponse = {
+ content: fullContent,
+ };
+
+ // Process tool calls
+ for (const toolCall of toolCalls) {
+ try {
+ if (toolCall.function.name === "provideLinks") {
+ const args = toolCall.function.arguments;
+ if (isValidJSON(args)) {
+ const toolResponse = JSON.parse(args) as { links?: InkeepLink[] };
+ result.links = toolResponse.links || [];
+ }
+ } else if (toolCall.function.name === "provideAIAnnotations") {
+ const args = toolCall.function.arguments;
+ if (isValidJSON(args)) {
+ const toolResponse = JSON.parse(args) as { aiAnnotations?: AIAnnotations };
+ result.aiAnnotations = toolResponse.aiAnnotations;
+ }
+ }
+ } catch (e) {
+ console.error("Error processing tool call in final response:", e);
+ }
+ }
+
+ onComplete(result);
+ });
+
+ stream.on("error", (err: Error) => {
+ console.error("Stream error:", err);
+ throw err;
+ });
Greptile
greptile
style: Consider handling the error here instead of re-throwing it, as this could lead to an unhandled promise rejection if the caller doesn't have a catch block.
diff block
retry: false,
refetchInterval: isLive ? 5_000 : undefined,
queryFn: async (query) => {
- const { timeFilter, userFilters, dbIncrement, timeZoneDifference } = query
- .queryKey[1] as BackendMetricsCall<T>["params"];
+ const {
+ timeFilter,
+ userFilters,
+ dbIncrement,
+ timeZoneDifference,
+ sortKey,
+ sortDirection,
+ } = query.queryKey[1] as BackendMetricsCall<T>["params"];
const res = fetch(endpoint, {
Greptile
greptile
logic: Missing await on fetch call. This could lead to unhandled promise rejections.
suggested fix
+ const res = await fetch(endpoint, {