Skip to content

feat!: replace magic string errors with typed Error subclasses #51

@untemps

Description

@untemps

Summary

⚠️ Breaking change

Replace the current magic-string error messages ([TIMEOUT], [ABORT], [EVENTS]) with dedicated Error subclasses. Consumers would use instanceof checks instead of fragile string matching.

Current state

// Library throws
new Error('[TIMEOUT]: Element #foo cannot be found after 500ms')
new Error('[ABORT]: Observation replaced by a new wait() call')
new Error('[EVENTS]: events array cannot be empty')

// Consumer must check
catch (e) {
    if ((e as Error).message.includes('[TIMEOUT]')) { ... }
}

Problems:

  • String matching is fragile: any message wording change is a silent breaking change
  • No TypeScript type narrowing from instanceof checks
  • Inconsistency: abort paths throw DOMException with name: 'AbortError' while timeout/events paths throw plain Error

Proposed API

// New exports
export class TimeoutError extends Error {
    readonly target: DOMTarget
    readonly timeout: number
    constructor(target: DOMTarget, timeout: number) {
        super(`Element "${target}" not found after ${timeout}ms`)
        this.name = 'TimeoutError'
        this.target = target
        this.timeout = timeout
    }
}

export class ObservationAbortedError extends Error {
    constructor(reason?: string) {
        super(reason ?? 'Observation replaced by a newer call')
        this.name = 'ObservationAbortedError'
    }
}

export class InvalidEventsError extends Error {
    constructor() {
        super('events array cannot be empty')
        this.name = 'InvalidEventsError'
    }
}

Consumer code:

import { DOMObserver, TimeoutError } from '@untemps/dom-observer'

try {
    await obs.wait('#foo', { timeout: 500 })
} catch (e) {
    if (e instanceof TimeoutError) {
        console.log(`Timed out waiting for ${e.target} after ${e.timeout}ms`)
    }
    if (e instanceof DOMException && e.name === 'AbortError') {
        // AbortSignal path — already uses DOMException, unchanged
    }
}

Breaking changes

  • Any consumer code that matches on .message.includes('[TIMEOUT]') etc. will need to migrate to instanceof checks.
  • The internal [TIMEOUT] prefix is removed from messages — if anything logs error messages, the format changes.

Migration guide

Before After
e.message.includes('[TIMEOUT]') e instanceof TimeoutError
e.message.includes('[ABORT]') e instanceof ObservationAbortedError
e.message.includes('[EVENTS]') e instanceof InvalidEventsError

Notes

  • The AbortSignal rejection path already uses DOMException (correct) and is unchanged.
  • TimeoutError storing target and timeout as structured properties allows callers to inspect the error programmatically without parsing a message string.
  • See the non-breaking stepping stone in issue feat: export error message constants #41 (export error constants) — that issue can be delivered first to help consumers migrate.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions