Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const executeTask = async (task: MaintenanceTask) => {
message = t('maintenance.error.taskFailed')
} catch (error) {
message = (error as Error)?.message
message = error instanceof Error ? error.message : undefined
}
toast.add({
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop-ui/src/stores/maintenanceTaskStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class MaintenanceTaskRunner {
this.error = undefined
return true
} catch (error) {
this.error = (error as Error)?.message
this.error = error instanceof Error ? error.message : String(error)
throw error
} finally {
this.executing = false
Expand Down
25 changes: 25 additions & 0 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,31 @@ export default defineConfig([
]
}
},
{
name: 'comfy/no-unsafe-error-assertion',
files: [
'src/**/*.ts',
'src/**/*.tsx',
'src/**/*.vue',
'apps/*/src/**/*.ts',
'apps/*/src/**/*.tsx',
'apps/*/src/**/*.vue'
],
ignores: ['**/*.test.ts', '**/*.spec.ts'],
rules: {
'no-restricted-syntax': [
'error',
{
// Bans `value as Error` and `value as Error & { ... }`.
// Use `error instanceof Error` narrowing or `toError()` from
// @/utils/errorUtil instead — see issue #11429.
selector: "TSAsExpression TSTypeReference[typeName.name='Error']",
message:
'Do not use `as Error` assertions. Use `instanceof Error` narrowing or `toError()` from @/utils/errorUtil instead. See issue #11429.'
}
]
}
},
{
files: ['**/*.spec.ts'],
ignores: ['browser_tests/tests/**/*.spec.ts', 'apps/*/e2e/**/*.spec.ts'],
Expand Down
11 changes: 9 additions & 2 deletions src/extensions/core/load3d/LoaderManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,15 @@ import type {
*/
function isNotFoundError(error: unknown): boolean {
if (!(error instanceof Error)) return false
const withResponse = error as Error & { response?: { status?: number } }
if (withResponse.response?.status === 404) return true
if (
'response' in error &&
typeof error.response === 'object' &&
error.response !== null &&
'status' in error.response &&
error.response.status === 404
) {
return true
}
return /\b404\b/.test(error.message)
}

Expand Down
5 changes: 3 additions & 2 deletions src/platform/cloud/onboarding/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as Sentry from '@sentry/vue'
import { isEmpty } from 'es-toolkit/compat'

import { api } from '@/scripts/api'
import { toError } from '@/utils/errorUtil'

interface UserCloudStatus {
status: 'active'
Expand Down Expand Up @@ -80,7 +81,7 @@ export async function getUserCloudStatus(): Promise<UserCloudStatus> {
} catch (error) {
// Only capture network errors (not HTTP errors we already captured)
if (!isHttpError(error, 'Failed to get user:')) {
captureApiError(error as Error, '/user', 'network_error')
captureApiError(toError(error), '/user', 'network_error')
}
throw error
}
Expand Down Expand Up @@ -176,7 +177,7 @@ export async function submitSurvey(
// Only capture network errors (not HTTP errors we already captured)
if (!isHttpError(error, 'Failed to submit survey:')) {
captureApiError(
error as Error,
toError(error),
'/settings',
'network_error',
undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { MediaRecorder as ExtendableMediaRecorder } from 'extendable-media-recor
import { onUnmounted, ref } from 'vue'

import { useAudioService } from '@/services/audioService'
import { toError } from '@/utils/errorUtil'

interface AudioRecorderOptions {
onRecordingComplete?: (audioBlob: Blob) => Promise<void>
Expand Down Expand Up @@ -62,7 +63,7 @@ export function useAudioRecorder(options: AudioRecorderOptions = {}) {
isRecording.value = true
} catch (err) {
if (options.onError) {
options.onError(err as Error)
options.onError(toError(err))
}
throw err
}
Expand Down
3 changes: 2 additions & 1 deletion src/services/gateway/registrySearchGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
NodePackSearchProvider,
SearchPacksResult
} from '@/types/searchServiceTypes'
import { toError } from '@/utils/errorUtil'

type RegistryNodePack = components['schemas']['Node']

Expand Down Expand Up @@ -152,7 +153,7 @@ export const useRegistrySearchGateway = (): NodePackSearchProvider => {
recordSuccess(providerState)
return result
} catch (error) {
lastError = error as Error
lastError = toError(error)
const providerState = providers[activeProviderIndex]
recordFailure(providerState, lastError)
console.warn(
Expand Down
74 changes: 74 additions & 0 deletions src/utils/errorUtil.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, expect, it } from 'vitest'

import { getErrorMessage, toError } from './errorUtil'

describe('toError', () => {
it('returns the same Error instance when given an Error', () => {
const err = new Error('boom')
expect(toError(err)).toBe(err)
})

it('preserves Error subclasses', () => {
class CustomError extends Error {}
const err = new CustomError('subclass')
expect(toError(err)).toBe(err)
expect(toError(err)).toBeInstanceOf(CustomError)
})

it('wraps a string as an Error message', () => {
const result = toError('plain string')
expect(result).toBeInstanceOf(Error)
expect(result.message).toBe('plain string')
})

it('wraps a number by stringifying it', () => {
const result = toError(42)
expect(result).toBeInstanceOf(Error)
expect(result.message).toBe('42')
})

it('wraps an object via JSON.stringify', () => {
const result = toError({ code: 'EBOOM', detail: 'nope' })
expect(result).toBeInstanceOf(Error)
expect(result.message).toBe('{"code":"EBOOM","detail":"nope"}')
})

it('falls back to String() when JSON.stringify throws (circular)', () => {
const obj: Record<string, unknown> = {}
obj.self = obj
const result = toError(obj)
expect(result).toBeInstanceOf(Error)
expect(result.message).toBe('[object Object]')
})

it('handles null and undefined', () => {
expect(toError(null).message).toBe('null')
expect(toError(undefined).message).toBe('undefined')
})
})

describe('getErrorMessage', () => {
it('returns the message of an Error', () => {
expect(getErrorMessage(new Error('boom'))).toBe('boom')
})

it('returns the value when given a string', () => {
expect(getErrorMessage('text')).toBe('text')
})

it('returns the message field of a plain object', () => {
expect(getErrorMessage({ message: 'from object' })).toBe('from object')
})

it('returns undefined for objects without a string message', () => {
expect(getErrorMessage({ code: 1 })).toBeUndefined()
expect(getErrorMessage({ message: 42 })).toBeUndefined()
})

it('returns undefined for null, undefined, numbers, booleans', () => {
expect(getErrorMessage(null)).toBeUndefined()
expect(getErrorMessage(undefined)).toBeUndefined()
expect(getErrorMessage(42)).toBeUndefined()
expect(getErrorMessage(true)).toBeUndefined()
})
})
37 changes: 37 additions & 0 deletions src/utils/errorUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Narrow an unknown caught value to an Error.
*
* Replaces unsafe `value as Error` assertions. When `value` is not already
* an Error instance, wraps it in a new Error whose message is the stringified
* input so downstream consumers (loggers, Sentry, toasts) always receive a
* usable Error object instead of `undefined.message`.
*/
export function toError(value: unknown): Error {
if (value instanceof Error) return value
if (typeof value === 'string') return new Error(value)
if (value === undefined) return new Error('undefined')
try {
const serialised = JSON.stringify(value)
return new Error(serialised ?? String(value))
} catch {
return new Error(String(value))
}
}

/**
* Extract a message from an unknown caught value without asserting its type.
* Returns `undefined` when the value carries no usable message.
*/
export function getErrorMessage(value: unknown): string | undefined {
if (value instanceof Error) return value.message
if (typeof value === 'string') return value
if (
typeof value === 'object' &&
value !== null &&
'message' in value &&
typeof (value as { message: unknown }).message === 'string'
) {
return (value as { message: string }).message
}
return undefined
}
Loading