Skip to content

feat: add $fetch.safe() for Go-like error handling#555

Open
productdevbook wants to merge 1 commit intounjs:mainfrom
productdevbook:feat/safe-fetch
Open

feat: add $fetch.safe() for Go-like error handling#555
productdevbook wants to merge 1 commit intounjs:mainfrom
productdevbook:feat/safe-fetch

Conversation

@productdevbook
Copy link
Copy Markdown

@productdevbook productdevbook commented Mar 30, 2026

Summary

  • Add $fetch.safe() that returns { data, error } instead of throwing
  • Type-safe discriminated union: SafeResult<T>
  • Works with $fetch.create() instances

Usage

// Before (try/catch)
try {
  const data = await $fetch('/api/users')
} catch (error) {
  if (isFetchError(error)) {
    console.log(error.status)
  }
}

// After (Go-like)
const { data, error } = await $fetch.safe('/api/users')
if (error) {
  console.log(error.status) // typed as IFetchError
  return
}
console.log(data) // typed as T

Type narrowing works automatically:

const result = await $fetch.safe<User>('/api/user/1')

if (result.error) {
  // result.data is undefined here
  console.error(result.error.status)
  return
}

// result.data is User here
console.log(result.data.name)

Test plan

  • Returns { data, error: undefined } on success
  • Returns { data: undefined, error } on HTTP error (404, 403)
  • Returns { data: undefined, error } on network error
  • Error has correct status/statusText properties
  • Works with $fetch.create() instances
  • All existing tests pass (33 tests)
  • Lint, typecheck, build pass

Resolves #370

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a new .safe() method for fetch operations that returns a structured result object containing either the successful response data or detailed error information, instead of throwing exceptions. This method handles all scenarios uniformly—successful HTTP responses, error statuses (4xx/5xx), and network failures—providing an alternative approach to error handling.

Add `$fetch.safe()` that returns `{ data, error }` instead of throwing.
Enables type-safe error handling without try/catch blocks.

Resolves unjs#370

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

📝 Walkthrough

Walkthrough

Added a $fetch.safe() method that wraps HTTP requests in try-catch, returning a tuple-like object with { data, error } instead of throwing exceptions. Updated type definitions to include the SafeResult type and safe method signature. Added comprehensive test coverage for success, HTTP error, and network failure scenarios.

Changes

Cohort / File(s) Summary
Core Implementation
src/fetch.ts
Added safe(request, options) method that wraps $fetch calls in try-catch, returning an object with data and error fields instead of throwing exceptions.
Type Definitions
src/types.ts
Added SafeResult<T, E> union type for non-throwing outcomes and extended $Fetch interface with safe method signature accepting the same parameters as regular $fetch calls.
Test Coverage
test/index.test.ts
Added comprehensive test suite covering successful responses, HTTP error responses (404, 403), network failures (localhost:1), type narrowing via error checking, and compatibility with $fetch.create(...) instances.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A fetch so safe, no throws to fear,
With { data, error } crystal clear,
Like Go's own way, we gently guide,
Each result wrapped up with pride! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding $fetch.safe() for Go-like error handling, which is the primary focus of this PR.
Linked Issues check ✅ Passed The PR fully implements all coding requirements from issue #370: provides Go-like error handling via $fetch.safe() returning {data, error}, includes type-safe discriminated union (SafeResult), and supports $fetch.create() instances.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing $fetch.safe() as specified in issue #370; no unrelated modifications to error handling, retry logic, or other functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/fetch.ts`:
- Around line 266-272: The catch block in $fetch.safe can return arbitrary
thrown values (including undefined), breaking the SafeResult discriminant;
change the catch to normalize any caught value into a proper IFetchError before
returning so error is never undefined on failure. Implement or call a helper
(e.g., normalizeFetchError) that accepts unknown, maps primitives/null/undefined
to a canonical IFetchError with at least a message and originalValue fields (and
preserves IFetchError objects passthrough), then return { data: undefined,
error: normalizedError } from $fetch.safe so SafeResult and consumers of
IFetchError remain type-safe.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d9cad8c-b945-4b8d-adf3-c8bd45b051bf

📥 Commits

Reviewing files that changed from the base of the PR and between dfbe3ca and d6c36e4.

📒 Files selected for processing (3)
  • src/fetch.ts
  • src/types.ts
  • test/index.test.ts

Comment thread src/fetch.ts
Comment on lines +266 to +272
$fetch.safe = async (request, options) => {
try {
const data = await $fetch(request, options);
return { data, error: undefined };
} catch (error) {
return { data: undefined, error: error as any };
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find and examine src/fetch.ts around lines 266-272
cd /
find . -name "fetch.ts" -type f 2>/dev/null | head -5

Repository: unjs/ofetch

Length of output: 87


🏁 Script executed:

# Read the fetch.ts file to understand the context
wc -l src/fetch.ts

Repository: unjs/ofetch

Length of output: 71


🏁 Script executed:

# Get the full context around the changed lines, including surrounding code
sed -n '250,285p' src/fetch.ts

Repository: unjs/ofetch

Length of output: 921


🏁 Script executed:

# Search for IFetchError type definition
rg "type IFetchError|interface IFetchError|class IFetchError" --type ts

Repository: unjs/ofetch

Length of output: 121


🏁 Script executed:

# Search for where user hooks/callbacks might throw errors
rg "hook\(|callback\(|onError|onRequest|onResponse" src/fetch.ts -A 3 -B 1

Repository: unjs/ofetch

Length of output: 1259


🏁 Script executed:

# Check if there are existing error handling or normalization patterns
rg "error as any|as unknown|instanceof Error" src/fetch.ts -B 2 -A 2

Repository: unjs/ofetch

Length of output: 312


🏁 Script executed:

# Find callHooks definition
rg "function callHooks|const callHooks|export.*callHooks" src/ -B 2 -A 10

Repository: unjs/ofetch

Length of output: 541


🏁 Script executed:

# Look at the complete onError function
sed -n '200,250p' src/fetch.ts

Repository: unjs/ofetch

Length of output: 1755


🏁 Script executed:

# Search for all throw statements in fetch.ts
rg "throw " src/fetch.ts -B 2 -A 1

Repository: unjs/ofetch

Length of output: 130


🏁 Script executed:

# Check the full $fetchRaw function to understand error path
sed -n '80,200p' src/fetch.ts

Repository: unjs/ofetch

Length of output: 3999


🏁 Script executed:

# Find createFetchError function
rg "function createFetchError|const createFetchError|export.*createFetchError" src/ -B 2 -A 15

Repository: unjs/ofetch

Length of output: 942


🏁 Script executed:

# Look for test cases that might show hooks throwing errors
fd "test\|spec\|\.test\|\.spec" --type f | head -5

Repository: unjs/ofetch

Length of output: 37


🏁 Script executed:

# Check if there are any tests for $fetch.safe
rg "\$fetch\.safe|safe\(" --type ts -A 5 -B 1

Repository: unjs/ofetch

Length of output: 1894


🏁 Script executed:

# Look at the complete onError function to see what it returns
sed -n '1,80p' src/fetch.ts

Repository: unjs/ofetch

Length of output: 2531


🏁 Script executed:

# Check the complete createFetchError implementation
sed -n '1,50p' src/error.ts

Repository: unjs/ofetch

Length of output: 1581


🏁 Script executed:

# Look for any existing error normalization or handling patterns
rg "instanceof Error|new Error|Error constructor" src/ --type ts -B 1 -A 1

Repository: unjs/ofetch

Length of output: 254


🏁 Script executed:

# Check if IFetchError is exported from types.ts
rg "export.*IFetchError|export interface IFetchError" src/types.ts -B 1 -A 3

Repository: unjs/ofetch

Length of output: 311


🏁 Script executed:

# Verify the type signature of $fetch.safe in types.ts
rg "safe.*\?" src/types.ts -B 3 -A 5

Repository: unjs/ofetch

Length of output: 37


🏁 Script executed:

# Check if there are any tests that intentionally throw from hooks
rg "throw.*undefined|throw null|onRequest.*throw|onResponse.*throw" test/ -B 3 -A 3

Repository: unjs/ofetch

Length of output: 37


Normalize caught values so failure never returns error: undefined.

The SafeResult type discriminant requires error: undefined only when data succeeds, and error: IFetchError when data fails. However, user hooks (onRequest, onResponse, etc.) can throw arbitrary values—including undefined, null, or primitives—which currently bypass type safety via the error as any cast. This can produce { data: undefined, error: undefined }, violating the discriminant contract.

Suggested fix
+import type { IFetchError } from "./types.ts";
+
+function normalizeSafeError(error: unknown): IFetchError {
+  if (error instanceof Error) {
+    return error as IFetchError;
+  }
+  return new Error(String(error ?? "Unknown fetch error")) as IFetchError;
+}
+
   $fetch.safe = async (request, options) => {
     try {
       const data = await $fetch(request, options);
       return { data, error: undefined };
     } catch (error) {
-      return { data: undefined, error: error as any };
+      return { data: undefined, error: normalizeSafeError(error) };
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fetch.ts` around lines 266 - 272, The catch block in $fetch.safe can
return arbitrary thrown values (including undefined), breaking the SafeResult
discriminant; change the catch to normalize any caught value into a proper
IFetchError before returning so error is never undefined on failure. Implement
or call a helper (e.g., normalizeFetchError) that accepts unknown, maps
primitives/null/undefined to a canonical IFetchError with at least a message and
originalValue fields (and preserves IFetchError objects passthrough), then
return { data: undefined, error: normalizedError } from $fetch.safe so
SafeResult and consumers of IFetchError remain type-safe.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Go-like syntax to handle errors

1 participant