You deployed a Firebase Cloud Function. From the frontend:
const result = await httpsCallable(functions, 'sendEmail')({to: 'x'});
You get:
FirebaseError: Function not found: sendEmail
Or a direct HTTP fetch returns:
404 Not Found
But the Firebase Console clearly lists sendEmail under Functions. Welcome to the most confusing Cloud Functions error — “visible in console” ≠ “callable from client.” The problem is always one of: name / region / deploy state.
Common causes
Ordered by hit rate, highest first.
1. Region mismatch between client and function
Most common. Function declares region: 'asia-east1', but the client SDK doesn’t specify a region and defaults to us-central1 — function isn’t there.
How to spot it:
- Function code:
onCall({ region: 'asia-east1' }, ...)or v1:functions.region('asia-east1').https.onCall(...) - Client:
getFunctions(app)with no region = us-central1 - Mismatch = not found
2. Function name typo / case mismatch
// function code
export const sendEmail = onCall(...);
// client
httpsCallable(functions, 'send-email') // ❌ kebab-case
httpsCallable(functions, 'sendmail') // ❌ typo
Firebase function names are export variable names — case-sensitive exact match.
How to spot it: firebase functions:list or copy exact name from console; compare with client.
3. Build silently failed during deploy
CLI says Deploy complete!, but one function got skipped due to a TS error or missing dep. Others succeeded; this one silently didn’t.
How to spot it: firebase deploy --only functions --debug and search “Failed to upload” or “skipped”.
4. v1 / v2 SDK confusion
Firebase Functions is split into v1 (firebase-functions) and v2 (firebase-functions/v2). Different endpoints, slightly different call shapes.
How to spot it: Check imports: import { onCall } from 'firebase-functions/v2/https' (v2) vs import * as functions from 'firebase-functions' (v1).
5. Function garbage-collected by deploy
If a previous deploy didn’t export sendEmail, Firebase deletes it. Next deploy you re-export but the deploy isn’t done — function is missing.
How to spot it: firebase functions:list shows whether it actually exists.
6. CORS blocks the request from ever leaving the browser
HTTPS triggers have CORS rules. Without setting them, cross-origin frontend calls get blocked client-side — looks like 404 but never made it to the server.
How to spot it: Network tab shows status 0 or CORS error, not 404.
Shortest path to fix
Step 1: List functions to confirm existence
firebase functions:list --project my-project-prod
# Example:
# ┌──────────────┬─────────┬───────────────┬─────────┐
# │ Function │ Version │ Trigger │ Region │
# ├──────────────┼─────────┼───────────────┼─────────┤
# │ sendEmail │ v2 │ https │ us-east1│
# └──────────────┴─────────┴───────────────┴─────────┘
Confirms name, version, region.
Step 2: Set region in the client SDK
// v2 SDK
import { getFunctions, httpsCallable } from 'firebase/functions';
// ❌ unspecified = us-central1
const fns = getFunctions(app);
// ✅ matches the function
const fns = getFunctions(app, 'us-east1');
const callSendEmail = httpsCallable(fns, 'sendEmail');
Step 3: Declare region in the function code
// v2
import { onCall } from 'firebase-functions/v2/https';
export const sendEmail = onCall(
{ region: 'us-east1', cors: true }, // explicit region
async (req) => { /* ... */ }
);
Step 4: Re-deploy and watch build logs
firebase deploy --only functions:sendEmail --debug 2>&1 | tee deploy.log
# Grep for issues
grep -i "error\|failed\|skipped" deploy.log
CLI surfaces build failures but they’re easy to miss in long output; --debug adds detail.
Step 5: Verify with the local emulator
firebase emulators:start --only functions
# Client connects to emulator
import { connectFunctionsEmulator } from 'firebase/functions';
if (location.hostname === 'localhost') {
connectFunctionsEmulator(fns, 'localhost', 5001);
}
Works on emulator = deploy / region issue. Fails on emulator too = code issue.
Step 6: Direct HTTP curl
# v2 onCall endpoint
curl -X POST https://us-east1-my-project.cloudfunctions.net/sendEmail \
-H "Content-Type: application/json" \
-d '{"data":{"to":"x"}}'
# 404 = truly not there or wrong region
# 200 / 401 / 403 = exists, different issue (auth / permission)
Step 7: v1 / v2 audit
// v1
import * as functions from 'firebase-functions';
export const sendEmail = functions.https.onCall((data, context) => ...);
// v2
import { onCall } from 'firebase-functions/v2/https';
export const sendEmail = onCall((req) => ...);
Different URL paths. Mixing them calls the wrong place. Pick one version repo-wide.
Prevention
- One region per project; write it in CLAUDE.md / README; all new functions use the same one
- Always pass region when initializing the client SDK — never rely on the default
- Centralize:
export const FUNCTIONS_REGION = 'us-east1'; both client and server import it - Post-deploy health-check script that curls every endpoint
- Add a CI typecheck step so build failures don’t slip into deploy
- Use camelCase for names (matches JS exports); don’t mix in kebab-case
- v1 → v2 migration: do it all at once; don’t half-v1, half-v2
- Add “function not found” metrics; alert when it spikes