feat: add $fetch.safe() for Go-like error handling#555
feat: add $fetch.safe() for Go-like error handling#555productdevbook wants to merge 1 commit intounjs:mainfrom
Conversation
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>
📝 WalkthroughWalkthroughAdded a Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~12 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
src/fetch.tssrc/types.tstest/index.test.ts
| $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 }; | ||
| } |
There was a problem hiding this comment.
🧩 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 -5Repository: unjs/ofetch
Length of output: 87
🏁 Script executed:
# Read the fetch.ts file to understand the context
wc -l src/fetch.tsRepository: 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.tsRepository: unjs/ofetch
Length of output: 921
🏁 Script executed:
# Search for IFetchError type definition
rg "type IFetchError|interface IFetchError|class IFetchError" --type tsRepository: 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 1Repository: 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 2Repository: unjs/ofetch
Length of output: 312
🏁 Script executed:
# Find callHooks definition
rg "function callHooks|const callHooks|export.*callHooks" src/ -B 2 -A 10Repository: unjs/ofetch
Length of output: 541
🏁 Script executed:
# Look at the complete onError function
sed -n '200,250p' src/fetch.tsRepository: unjs/ofetch
Length of output: 1755
🏁 Script executed:
# Search for all throw statements in fetch.ts
rg "throw " src/fetch.ts -B 2 -A 1Repository: unjs/ofetch
Length of output: 130
🏁 Script executed:
# Check the full $fetchRaw function to understand error path
sed -n '80,200p' src/fetch.tsRepository: unjs/ofetch
Length of output: 3999
🏁 Script executed:
# Find createFetchError function
rg "function createFetchError|const createFetchError|export.*createFetchError" src/ -B 2 -A 15Repository: 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 -5Repository: 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 1Repository: 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.tsRepository: unjs/ofetch
Length of output: 2531
🏁 Script executed:
# Check the complete createFetchError implementation
sed -n '1,50p' src/error.tsRepository: 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 1Repository: 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 3Repository: 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 5Repository: 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 3Repository: 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.
Summary
$fetch.safe()that returns{ data, error }instead of throwingSafeResult<T>$fetch.create()instancesUsage
Type narrowing works automatically:
Test plan
{ data, error: undefined }on success{ data: undefined, error }on HTTP error (404, 403){ data: undefined, error }on network error$fetch.create()instancesResolves #370
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes
.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.