diff --git a/apps/sim/app/api/workflows/[id]/execute/route.ts b/apps/sim/app/api/workflows/[id]/execute/route.ts index 217d26f9b..856a1a3c9 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.ts @@ -30,6 +30,7 @@ import { normalizeName } from '@/executor/constants' import { ExecutionSnapshot } from '@/executor/execution/snapshot' import type { ExecutionMetadata, IterationContext } from '@/executor/execution/types' import type { NormalizedBlockOutput, StreamingExecution } from '@/executor/types' +import { hasExecutionResult } from '@/executor/utils/errors' import { Serializer } from '@/serializer' import { CORE_TRIGGER_TYPES, type CoreTriggerType } from '@/stores/logs/filters/types' @@ -467,17 +468,17 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: } return NextResponse.json(filteredResult) - } catch (error: any) { - const errorMessage = error.message || 'Unknown error' + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' logger.error(`[${requestId}] Non-SSE execution failed: ${errorMessage}`) - const executionResult = error.executionResult + const executionResult = hasExecutionResult(error) ? error.executionResult : undefined return NextResponse.json( { success: false, output: executionResult?.output, - error: executionResult?.error || error.message || 'Execution failed', + error: executionResult?.error || errorMessage || 'Execution failed', metadata: executionResult?.metadata ? { duration: executionResult.metadata.duration, @@ -788,11 +789,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: // Cleanup base64 cache for this execution await cleanupExecutionBase64Cache(executionId) - } catch (error: any) { - const errorMessage = error.message || 'Unknown error' + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error' logger.error(`[${requestId}] SSE execution failed: ${errorMessage}`) - const executionResult = error.executionResult + const executionResult = hasExecutionResult(error) ? error.executionResult : undefined sendEvent({ type: 'execution:error', diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index fbd432c12..1c0cbcc7a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -16,6 +16,7 @@ import { } from '@/lib/workflows/triggers/triggers' import { useCurrentWorkflow } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-current-workflow' import type { BlockLog, ExecutionResult, StreamingExecution } from '@/executor/types' +import { hasExecutionResult } from '@/executor/utils/errors' import { coerceValue } from '@/executor/utils/start-block' import { subscriptionKeys } from '@/hooks/queries/subscription' import { useExecutionStream } from '@/hooks/use-execution-stream' @@ -76,17 +77,6 @@ function normalizeErrorMessage(error: unknown): string { return WORKFLOW_EXECUTION_FAILURE_MESSAGE } -function isExecutionResult(value: unknown): value is ExecutionResult { - if (!isRecord(value)) return false - return typeof value.success === 'boolean' && isRecord(value.output) -} - -function extractExecutionResult(error: unknown): ExecutionResult | null { - if (!isRecord(error)) return null - const candidate = error.executionResult - return isExecutionResult(candidate) ? candidate : null -} - export function useWorkflowExecution() { const queryClient = useQueryClient() const currentWorkflow = useCurrentWorkflow() @@ -1138,11 +1128,11 @@ export function useWorkflowExecution() { const handleExecutionError = (error: unknown, options?: { executionId?: string }) => { const normalizedMessage = normalizeErrorMessage(error) - const executionResultFromError = extractExecutionResult(error) let errorResult: ExecutionResult - if (executionResultFromError) { + if (hasExecutionResult(error)) { + const executionResultFromError = error.executionResult const logs = Array.isArray(executionResultFromError.logs) ? executionResultFromError.logs : [] errorResult = { diff --git a/apps/sim/executor/execution/engine.ts b/apps/sim/executor/execution/engine.ts index 58792ef2b..05e7e0484 100644 --- a/apps/sim/executor/execution/engine.ts +++ b/apps/sim/executor/execution/engine.ts @@ -13,7 +13,7 @@ import type { PausePoint, ResumeStatus, } from '@/executor/types' -import { normalizeError } from '@/executor/utils/errors' +import { attachExecutionResult, normalizeError } from '@/executor/utils/errors' const logger = createLogger('ExecutionEngine') @@ -170,8 +170,8 @@ export class ExecutionEngine { metadata: this.context.metadata, } - if (error && typeof error === 'object') { - ;(error as any).executionResult = executionResult + if (error instanceof Error) { + attachExecutionResult(error, executionResult) } throw error } diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index 7bd295502..6759af5ff 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -11,6 +11,7 @@ import type { ExecutionResult, StreamingExecution, } from '@/executor/types' +import { hasExecutionResult } from '@/executor/utils/errors' import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http' import { parseJSON } from '@/executor/utils/json' import { lazyCleanupInputMapping } from '@/executor/utils/lazy-cleanup' @@ -148,8 +149,9 @@ export class WorkflowBlockHandler implements BlockHandler { const originalError = error.message || 'Unknown error' let childTraceSpans: WorkflowTraceSpan[] = [] let executionResult: ExecutionResult | undefined - if (error.executionResult?.logs) { - executionResult = error.executionResult as ExecutionResult + + if (hasExecutionResult(error) && error.executionResult.logs) { + executionResult = error.executionResult logger.info(`Extracting child trace spans from error.executionResult`, { hasLogs: (executionResult.logs?.length ?? 0) > 0, diff --git a/apps/sim/executor/utils/errors.ts b/apps/sim/executor/utils/errors.ts index 0f078d84e..3144d9c71 100644 --- a/apps/sim/executor/utils/errors.ts +++ b/apps/sim/executor/utils/errors.ts @@ -1,6 +1,33 @@ -import type { ExecutionContext } from '@/executor/types' +import type { ExecutionContext, ExecutionResult } from '@/executor/types' import type { SerializedBlock } from '@/serializer/types' +/** + * Interface for errors that carry an ExecutionResult. + * Used when workflow execution fails and we want to preserve partial results. + */ +export interface ErrorWithExecutionResult extends Error { + executionResult: ExecutionResult +} + +/** + * Type guard to check if an error carries an ExecutionResult. + */ +export function hasExecutionResult(error: unknown): error is ErrorWithExecutionResult { + return ( + error instanceof Error && + 'executionResult' in error && + error.executionResult != null && + typeof error.executionResult === 'object' + ) +} + +/** + * Attaches an ExecutionResult to an error for propagation to parent workflows. + */ +export function attachExecutionResult(error: Error, executionResult: ExecutionResult): void { + Object.assign(error, { executionResult }) +} + export interface BlockExecutionErrorDetails { block: SerializedBlock error: Error | string diff --git a/apps/sim/hooks/use-execution-stream.ts b/apps/sim/hooks/use-execution-stream.ts index 3516c9366..b9d1cc685 100644 --- a/apps/sim/hooks/use-execution-stream.ts +++ b/apps/sim/hooks/use-execution-stream.ts @@ -100,8 +100,13 @@ export function useExecutionStream() { }) if (!response.ok) { - const error = await response.json() - throw new Error(error.error || 'Failed to start execution') + const errorResponse = await response.json() + const error = new Error(errorResponse.error || 'Failed to start execution') + // Attach the execution result from server response for error handling + if (errorResponse && typeof errorResponse === 'object') { + Object.assign(error, { executionResult: errorResponse }) + } + throw error } if (!response.body) {