moeru-ai/airi · error

Tweet element not found

Error message

Tweet element not found

What it means

Inside getTweetDetails, after waiting for the tweet selector to appear, page.$(SELECTORS.TIMELINE.TWEET) is called again and returns null. This means the element that satisfied waitForSelector is no longer present (or the selector matches transient loading skeletons), so the code refuses to proceed with a null handle.

Source

Thrown at integrations/twitter-services/src/core/services/tweet.ts:279

    catch (error: unknown) {
      console.error('Error posting tweet:', error)
      throw new Error(`Failed to post tweet: ${errorToMessage(error)}`)
    }
  }

  /**
   * Gets detailed information about a specific tweet
   */
  async function getTweetDetails(tweetId: string): Promise<TweetDetail> {
    try {
      const page = ctx.page
      await page.goto(`${TWITTER_BASE_URL}/i/status/${tweetId}`)
      await page.waitForSelector(SELECTORS.TIMELINE.TWEET)

      // Get the main tweet element
      const tweetElement = await page.$(SELECTORS.TIMELINE.TWEET)
      if (!tweetElement) {
        throw new Error('Tweet element not found')
      }

      // Use the TweetParser to extract the main tweet data
      const mainTweet = await TweetParser.extractTweetData(page, tweetElement)
      if (!mainTweet) {
        throw new Error('Failed to extract tweet data')
      }

      // Check for quoted tweet
      let quotedTweet: Tweet | undefined
      const quotedTweetElement = await page.$('[data-testid="quotedTweet"]')
      if (quotedTweetElement) {
        const extractedQuotedTweet = await TweetParser.extractTweetData(page, quotedTweetElement)
        if (extractedQuotedTweet) {
          quotedTweet = extractedQuotedTweet
        }
      }

View on GitHub (pinned to 27111382b4)

Solutions

  1. Validate tweetId format and confirm the tweet exists before relying on getTweetDetails.
  2. Tighten SELECTORS.TIMELINE.TWEET so it matches only the real article rather than loading placeholders.
  3. After goto, detect a tombstone/login redirect and surface a more specific error.
  4. Retry once after a short delay in case the element unmounted due to client-side hydration.

Example fix

// before
const tweetElement = await page.$(SELECTORS.TIMELINE.TWEET)
if (!tweetElement) {
  throw new Error('Tweet element not found')
}

// after
await page.waitForSelector(SELECTORS.TIMELINE.TWEET, { timeout: 10000 })
const tweetElement = await page.$(SELECTORS.TIMELINE.TWEET)
if (!tweetElement) {
  const tombstone = await page.$('[data-testid="cellInnerDiv"] [role="button"]')
  throw new Error(tombstone ? 'Tweet unavailable (deleted/private)' : 'Tweet element not found')
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!/^\d+$/.test(tweetId)) {
  throw new Error(`Invalid tweet id: ${tweetId}`)
}

Type guard

function isTweetElement(el: { evaluate?: Function } | null | undefined): el is { evaluate: Function } {
  return !!el && typeof el.evaluate === 'function'
}

Try / catch

try {
  return await getTweetDetails(tweetId)
}
catch (err) {
  if (/not found|unavailable/i.test((err as Error).message)) {
    return null // treat as soft miss
  }
  throw err
}

Prevention

When it happens

Trigger: The tweet was deleted, made private, or age-restricted between navigation and extraction; SELECTORS.TIMELINE.TWEET matches a placeholder/skeleton that unmounts; the page redirected to a login or error page after the initial selector match; tweetId does not correspond to a real status.

Common situations: Looking up a tweet ID scraped from an old data set where the tweet no longer exists; running against an account whose session cannot view the target tweet; selector drift where TWEET matches a tombstone element.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/248a6aec4e7f2a7b. Report an issue: GitHub.