diff --git a/apps/backend/src/lib/freestyle.tsx b/apps/backend/src/lib/freestyle.tsx index 03acc0ed0..bd0a4d229 100644 --- a/apps/backend/src/lib/freestyle.tsx +++ b/apps/backend/src/lib/freestyle.tsx @@ -1,4 +1,5 @@ import { traceSpan } from '@/utils/telemetry'; +import { StackAssertionError, captureError, errorToNiceString } from '@stackframe/stack-shared/dist/utils/errors'; import { FreestyleSandboxes } from 'freestyle-sandboxes'; export class TracedFreestyleSandboxes { diff --git a/apps/backend/src/oauth/providers/base.tsx b/apps/backend/src/oauth/providers/base.tsx index 8f9681e00..a31745270 100644 --- a/apps/backend/src/oauth/providers/base.tsx +++ b/apps/backend/src/oauth/providers/base.tsx @@ -13,7 +13,7 @@ export type TokenSet = { function processTokenSet(providerName: string, tokenSet: OIDCTokenSet, defaultAccessTokenExpiresInMillis?: number): TokenSet { if (!tokenSet.access_token) { - throw new StackAssertionError("No access token received", { tokenSet }); + throw new StackAssertionError(`No access token received from ${providerName}.`, { tokenSetKeys: Object.keys(tokenSet), providerName }); } // if expires_in or expires_at provided, use that diff --git a/apps/backend/src/prisma-client.tsx b/apps/backend/src/prisma-client.tsx index 13218a24e..083066a22 100644 --- a/apps/backend/src/prisma-client.tsx +++ b/apps/backend/src/prisma-client.tsx @@ -6,7 +6,7 @@ import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dis import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { globalVar } from "@stackframe/stack-shared/dist/utils/globals"; import { deepPlainEquals, filterUndefined, typedFromEntries, typedKeys } from "@stackframe/stack-shared/dist/utils/objects"; -import { ignoreUnhandledRejection } from "@stackframe/stack-shared/dist/utils/promises"; +import { concatStacktracesIfRejected, ignoreUnhandledRejection } from "@stackframe/stack-shared/dist/utils/promises"; import { Result } from "@stackframe/stack-shared/dist/utils/results"; import { isPromise } from "util/types"; import { runMigrationNeeded } from "./auto-migrations"; @@ -305,8 +305,10 @@ async function rawQueryArray[]>(tx: PrismaClientTransact const postProcessed = combinedQuery.postProcess(rawResult as any); // If the postProcess is async, postProcessed is a Promise. If that Promise is rejected, it will cause an unhandled promise rejection. // We don't want that, because Vercel crashes on unhandled promise rejections. + // We also want to concat the current stack trace to the error, so we can see where the rawQuery function was called if (isPromise(postProcessed)) { ignoreUnhandledRejection(postProcessed); + concatStacktracesIfRejected(postProcessed); } return postProcessed; diff --git a/docs/src/app/not-found.tsx b/docs/src/app/not-found.tsx index 23b04298e..08dd0ff38 100644 --- a/docs/src/app/not-found.tsx +++ b/docs/src/app/not-found.tsx @@ -1,11 +1,8 @@ -import Link from "next/link"; export default function NotFound() { return (
-

Not Found

-

Could not find requested resource

- Return Home +

404 Not Found

); } diff --git a/packages/stack-shared/src/utils/errors.tsx b/packages/stack-shared/src/utils/errors.tsx index 2a4175edb..e14e7e800 100644 --- a/packages/stack-shared/src/utils/errors.tsx +++ b/packages/stack-shared/src/utils/errors.tsx @@ -28,6 +28,8 @@ function removeStacktraceNameLine(stack: string): string { /** * Concatenates the (original) stacktraces of the given errors onto the first. * + * Note: Very often, the concatStacktracesIfRejected function in promises.tsx is an easier way to use this function. + * * Useful when you invoke an async function to receive a promise without awaiting it immediately. Browsers are smart * enough to keep track of the call stack in async function calls when you invoke `.then` within the same async tick, * but if you don't, the stacktrace will be lost. @@ -36,7 +38,7 @@ function removeStacktraceNameLine(stack: string): string { * * ```tsx * async function log() { - * await wait(0); // simulate an put the task on the event loop + * await wait(0); // put the task on the event loop * console.log(new Error().stack); * } * diff --git a/packages/stack-shared/src/utils/promises.tsx b/packages/stack-shared/src/utils/promises.tsx index 2e9291d6e..7ef0835c5 100644 --- a/packages/stack-shared/src/utils/promises.tsx +++ b/packages/stack-shared/src/utils/promises.tsx @@ -1,5 +1,5 @@ import { KnownError } from ".."; -import { StackAssertionError, captureError, concatStacktraces } from "./errors"; +import { StackAssertionError, captureError, concatStacktraces, errorToNiceString } from "./errors"; import { DependenciesMap } from "./maps"; import { Result } from "./results"; import { generateUuid } from "./uuids"; @@ -243,6 +243,20 @@ import.meta.vitest?.test("ignoreUnhandledRejection", async ({ expect }) => { await expect(rejectPromise).rejects.toBe(error); }); +/** + * See concatStacktraces for more information. + */ +export function concatStacktracesIfRejected(promise: Promise): void { + const currentError = new Error(); + promise.catch(error => { + if (error instanceof Error) { + concatStacktraces(error, currentError); + } else { + // we can only concatenate errors, so we'll just ignore the non-error + } + }); +} + export async function wait(ms: number) { if (!Number.isFinite(ms) || ms < 0) { throw new StackAssertionError(`wait() requires a non-negative integer number of milliseconds to wait. (found: ${ms}ms)`); @@ -336,18 +350,19 @@ export function runAsynchronously( if (typeof promiseOrFunc === "function") { promiseOrFunc = promiseOrFunc(); } - const duringError = new Error(); - promiseOrFunc?.catch(error => { - options.onError?.(error); - const newError = new StackAssertionError( - "Uncaught error in asynchronous function: " + errorToNiceString(error), - { cause: error }, - ); - concatStacktraces(newError, duringError); - if (!options.noErrorLogging) { - captureError("runAsynchronously", newError); - } - }); + if (promiseOrFunc) { + concatStacktracesIfRejected(promiseOrFunc); + promiseOrFunc.catch(error => { + options.onError?.(error); + const newError = new StackAssertionError( + "Uncaught error in asynchronous function: " + errorToNiceString(error), + { cause: error }, + ); + if (!options.noErrorLogging) { + captureError("runAsynchronously", newError); + } + }); + } } import.meta.vitest?.test("runAsynchronously", ({ expect }) => { // Simple test to verify the function exists and can be called