yikart/AiToEarn · error · Error

Save failed

Error message

Save failed

What it means

In ChatDetailPage, saving a renamed task calls the update-title API and expects result.code === 0. When the response code is non-zero (or result is falsy), it throws Error('Save failed') after skipping the local title update.

Source

Thrown at project/aitoearn-web/src/app/[lng]/chat/[taskId]/page.tsx:322

    catch {
      setIsFavorited(!newValue) // 失败回滚
      toast.error(t('message.error'))
    }
  }, [isFavorited, taskId, t])

  /**
   * 保存标题 - 乐观更新
   */
  const handleSaveTitle = useCallback(
    async (newTitle: string) => {
      const result = await agentApi.updateTaskTitle(taskId, newTitle)
      if (result && result.code === 0) {
        // 更新本地 task 状态,触发页面标题和 header 标题更新
        updateTaskTitle(newTitle)
        toast.success(t('task.titleUpdated'))
      }
      else {
        throw new Error('Save failed')
      }
    },
    [taskId, t, updateTaskTitle],
  )

  // 加载中状态(仅非活跃任务显示骨架屏)
  if (isLoading && !isActiveTask) {
    return <ChatLoadingSkeleton />
  }

  return (
    <div className="flex flex-col h-full">
      {/* 顶部导航 */}
      <ChatHeader
        title={task?.title}
        defaultTitle={t('task.newChat')}
        isGenerating={isGenerating}
        progress={progress}

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Log the full result object (code + message) to see the backend's business error reason
  2. Refresh/re-fetch the task to confirm taskId is still valid, then retry
  3. Re-authenticate if the error envelope indicates auth failure (401-style code with HTTP 200)
  4. Validate the new title (non-empty, length limits) before calling the API and surface the API's message instead of the generic 'Save failed'

Example fix

// before
else {
  throw new Error('Save failed')
}
// after
else {
  throw new Error(`Save failed: ${result?.message ?? 'unknown error'} (code: ${result?.code})`)
}
Defensive patterns

Strategy: try-catch

Validate before calling

const newTitle = title.trim()
if (!newTitle || newTitle.length > 100) { toast.error('标题不能为空且需小于100字'); return }
if (!taskId) return

Type guard

function isApiSuccess(result: unknown): result is { code: 0; data?: unknown } {
  return typeof result === 'object' && result !== null && (result as any).code === 0
}

Try / catch

try {
  await saveTitle(newTitle)
} catch (e) {
  if (e.message === 'Save failed') {
    toast.error('保存失败,请刷新页面后重试')
    await refetchTask(taskId) // resync local state with server
    return
  }
  throw e
}

Prevention

When it happens

Trigger: The rename-task API returns { code: <non-zero> } (business error: invalid taskId, permission denied, validation failure) or returns no parsable result — even when the HTTP status was 200.

Common situations: Task already deleted in another tab (stale taskId), session/token expired so the backend returns an error envelope with HTTP 200, empty or invalid new title rejected by validation, or network layer returning an undefined result.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/adb3e18aa7cb38f6. Report an issue: GitHub.