You deployed a Firebase Cloud Function. From the frontend:
const result = await httpsCallable(functions, 'sendEmail')({ to: 'x' });
You get one of these:
FirebaseError: Function not found: sendEmail
FirebaseError: internal
Or a direct HTTP fetch returns:
404 Not Found
But the Firebase Console clearly lists sendEmail under Functions. This is the most confusing Cloud Functions failure: “visible in console” does not mean “callable from this client.” The cause is almost always one of three things — region, name, or deploy state.
Fastest fix (works ~70% of the time): your function runs in a region the client SDK isn’t pointing at. Run firebase functions:list, read the actual region, and pass that exact region to getFunctions(app, 'us-east1') on the client. Details and the other buckets below.
Which bucket are you in?
Run one command first, then match the symptom:
firebase functions:list --project my-project-prod
| Symptom | Most likely cause | Jump to |
|---|---|---|
| Function appears in the list, but in a region your client doesn’t set | Region mismatch | Step 2 |
| Function not in the list at all | Deploy skipped / build failed / GC’d | Step 4 |
| Name in list differs in case/spelling from your call | Name mismatch | Cause 2 |
Browser console shows a CORS error, status 0, or a blocked preflight | CORS, not a missing function | Cause 6 |
curl returns 200/401/403 but the SDK fails | v1/v2 path or auth issue, not “not found” | Step 6 |
Common causes
Ordered by hit rate, highest first.
1. Region mismatch between client and function
Most common by far. The function declares region: 'asia-east1', but the client SDK doesn’t specify a region. The JS client SDK still defaults to us-central1 when you call getFunctions(app) with no region argument — so it looks for the function in us-central1, where it isn’t.
One thing changed in recent Firebase CLI versions and trips people up: the CLI no longer unconditionally deploys to us-central1. It now picks a region based on your project config and the region of the triggering data source (Firestore, Storage) when it can infer one. So a function can land in a region you never explicitly chose, while your client is still hard-defaulting to us-central1. Always read the deployed region from functions:list rather than assuming.
How to spot it
- Function code:
onCall({ region: 'asia-east1' }, ...)(v2) orfunctions.region('asia-east1').https.onCall(...)(v1) - Client:
getFunctions(app)with no second argument resolves tous-central1 - A v2 callable mismatch frequently surfaces as a CORS / preflight error or
FirebaseError: internalrather than a clean “not found,” because the SDK hits a URL that doesn’t exist and the preflight gets no valid response. Don’t be misled by the wording.
2. Function name typo / case mismatch
// function code
export const sendEmail = onCall(...);
// client
httpsCallable(functions, 'send-email') // wrong: kebab-case
httpsCallable(functions, 'sendmail') // wrong: typo
Firebase function names are the exported variable names — exact, case-sensitive match.
How to spot it: copy the exact name from firebase functions:list or the console and diff it against the string in your client call.
3. Build silently failed during deploy
The CLI prints Deploy complete!, but one function got skipped because of a TypeScript error or a missing dependency. The others succeeded; this one silently didn’t ship.
How to spot it: firebase deploy --only functions --debug and search the output for Failed to upload, skipped, or a TypeScript compile error near the top.
4. v1 / v2 SDK confusion
Firebase Functions splits into v1 (firebase-functions) and v2 (firebase-functions/v2). They deploy to different endpoints and have slightly different call shapes. v2 runs on Cloud Run under the hood, which is why v2 functions get two URLs (more on that in Step 6).
How to spot it: check imports — import { onCall } from 'firebase-functions/v2/https' (v2) vs import * as functions from 'firebase-functions' (v1).
5. Function deleted by a previous deploy (garbage collection)
If an earlier deploy didn’t export sendEmail, Firebase treats it as removed and deletes it from the project. If you later re-export it but the deploy hasn’t finished (or failed), the function genuinely isn’t there.
How to spot it: firebase functions:list shows whether it currently exists at all.
6. CORS blocks the request before it leaves the browser
HTTPS-callable triggers handle the CORS preflight automatically only when CORS is configured. For v2, set cors: true (or an explicit origin allowlist) in the function options. Without it, a cross-origin call from your frontend gets blocked client-side — which can look like a 404 but never reached the server.
How to spot it: the browser Network tab shows status 0 or an explicit CORS policy error (for example: Access to fetch at 'https://us-central1-my-project.cloudfunctions.net/sendEmail' from origin 'https://my-project.web.app' has been blocked by CORS policy), not a clean 404 from the function itself.
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│
# └──────────────┴─────────┴───────────────┴─────────┘
This confirms three things at once: the exact name, the generation (v1/v2), and the region. If sendEmail isn’t in this list, skip to Step 4 — your problem is deploy, not region.
Step 2: Set the region on the client SDK
// v2 SDK
import { getFunctions, httpsCallable } from 'firebase/functions';
// wrong: no region argument resolves to us-central1
const fns = getFunctions(app);
// right: matches the region from functions:list
const fns = getFunctions(app, 'us-east1');
const callSendEmail = httpsCallable(fns, 'sendEmail');
The second argument to getFunctions can be either a region (for example us-east1) or a custom domain hosting your callables. Use the exact region string you saw in Step 1.
Step 3: Declare the region in the function code
// v2
import { onCall } from 'firebase-functions/v2/https';
export const sendEmail = onCall(
{ region: 'us-east1', cors: true }, // explicit region + CORS
async (req) => { /* ... */ }
);
Pinning the region in code means the deploy can’t silently relocate it (see Cause 1 about CLI auto-region behavior).
Step 4: Re-deploy and actually read the build log
firebase deploy --only functions:sendEmail --debug 2>&1 | tee deploy.log
# Grep for the failure modes that hide in long output
grep -i "error\|failed\|skipped\|not deployed" deploy.log
Deploy complete! is not proof a specific function shipped. The CLI does surface build failures, but they scroll past in long output; --debug plus tee lets you search them.
Step 5: Verify with the local emulator
firebase emulators:start --only functions
// Client connects to the emulator instead of production
import { connectFunctionsEmulator } from 'firebase/functions';
if (location.hostname === 'localhost') {
connectFunctionsEmulator(fns, '127.0.0.1', 5001);
}
Works on the emulator but not in production = a deploy or region issue. Fails on the emulator too = a code issue. (Use 127.0.0.1 rather than localhost if your runtime resolves localhost to IPv6 and the emulator only binds IPv4.)
Step 6: curl the endpoint directly
A v2 function exposes two URLs: a deterministic cloudfunctions.net URL and a Cloud Run *.a.run.app URL with a random hash. The cloudfunctions.net form is the easy one to construct by hand:
# v2 onCall endpoint (deterministic URL)
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 in the URL
# 200 / 401 / 403 = it exists; your issue is auth/permission, not "not found"
A callable expects the POST body wrapped in a data key, exactly as shown. If curl succeeds but the SDK fails, the function is fine and you’re back to a client-side region or name mismatch.
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) => { /* ... */ });
The two generations resolve to different URL paths, so mixing them calls the wrong place. Pick one generation repo-wide. New functions should start on v2; migrate v1 functions when you have bandwidth.
How to confirm it’s fixed
firebase functions:listshowssendEmailin the expected region and generation.- The same region string appears in
getFunctions(app, '<region>')on the client. - The deterministic
curlreturns200(or401/403, which still proves it exists). - The frontend call resolves without
Function not found,internal, or a CORS error in the Network tab.
If all four pass, the not-found is genuinely gone — anything left is auth or business-logic, not routing.
Prevention
- One region per project. Write it in the README, and have every new function use it.
- Always pass the region when initializing the client SDK; never rely on the default.
- Centralize it:
export const FUNCTIONS_REGION = 'us-east1';imported by both client and server. - Run a post-deploy health check that
curls every endpoint and fails CI on a non-2xx/4xx. - Add a CI typecheck step so a TypeScript error can’t slip into a “successful” deploy.
- Use camelCase names that match the JS export; don’t mix in kebab-case.
- Migrate v1 to v2 in one pass; don’t run half v1, half v2.
- After any CLI upgrade, re-check deployed regions — the CLI’s default region behavior has changed across recent versions.
FAQ
Why does the console show the function but the client can’t find it?
The console lists every deployed function across all regions. The client SDK only looks in the single region you configured (or us-central1 by default). Visible in console, wrong region for the client = not found.
I never set a region. Where did my function deploy?
Recent Firebase CLI versions infer a region from your project config and trigger source instead of always using us-central1. Run firebase functions:list to see where it actually landed, and pin the region in code so it stops moving.
My error says internal or it’s a CORS error, not “Function not found.” Same problem?
Often yes. A v2 callable pointed at the wrong region hits a non-existent URL; the failed preflight surfaces as a CORS error or FirebaseError: internal rather than a clean not-found. Fix the region first before chasing CORS.
curl returns 200 but the SDK still fails. Why?
The function exists and is reachable, so it’s not a deploy or routing problem. Recheck the region passed to getFunctions and the exact name string in httpsCallable — those are the two client-side levers left.
Do I need to set CORS for callable functions?
For v2 onCall, set cors: true (or an explicit origin list) in the function options so the SDK’s preflight succeeds from your web origin. v1 callables handled this automatically; v2 wants it declared.
Related
External references: Cloud Functions locations · Call functions from your app