An empty array is the most politely misleading response an API can return. It means one of several things: there genuinely are no results matching the query, the filter has a typo and is matching nothing, the user doesn't have permission to see any records, the upstream dependency is down and returning nothing, or the query logic has a bug that always returns empty on this input. The client gets {"results": []} and has no idea which situation it's in.
This creates silent failures that are hard to diagnose and worse for users than explicit errors would be.
The Three Failure Modes
Typo in the filter value. A user submits a search for status=activ instead of status=active. Your API accepts the parameter (you don't validate filter values against an allowlist), returns zero results, and the user assumes their account has no active records. They don't — they just spelled the filter wrong. Your API has no way to tell them this because it treated an unknown status value as a valid filter that returned no matches.
Permission scoping returns nothing. A user with read permissions on a subset of records queries an endpoint. Their permissions happen to exclude all records that would match the query. They get an empty array. Is this correct — they have access to nothing? Or have they lost permissions they should have? Or is there a bug in the permission logic? The empty array is ambiguous between "no authorized records matching this query" and "something is wrong with your authorization."
Upstream is down. Your API depends on a third-party data source. The source returns an error or times out. Your error handling catches the exception and returns an empty array to the client rather than propagating the failure. Now the client thinks there's no data instead of knowing the data source is unavailable. The worst version of this: you cache the empty array and serve it for the next hour.
Correct Patterns
Validate filter values against an allowlist. If your API accepts status as a filter, it should know the valid values and return 400 for anything else:
VALID_STATUSES = {'active', 'inactive', 'pending', 'cancelled'}
def get_records(status=None):
if status and status not in VALID_STATUSES:
raise ValidationError(
f"Invalid status '{status}'. Valid values: {sorted(VALID_STATUSES)}"
)
# ... query logicThis turns a silent mismatch into an explicit error with a useful message. The client learns immediately that their filter is wrong rather than spending time wondering why there's no data.
Return metadata with your empty arrays. When you do legitimately return empty results, give the client context to distinguish "nothing here" from "something is wrong":
{
"results": [],
"total": 0,
"filters_applied": {"status": "active", "since": "2026-01-01"},
"filters_valid": true
}The X-Total-Count header, pagination.total, or a top-level total field tells the client that the server processed the query successfully and found zero matching records — not that something failed.
Distinguish permission-empty from no-results-empty. If a user queries something and gets zero results because of permissions (not because there are actually no records), consider whether that distinction matters for their workflow. In many cases it does: a user who has never had any records needs to see an onboarding message; a user who has records but can't see them needs a different message or contact path. The API can return both the empty array and a flag indicating permission scope was involved.
Don't convert upstream failures to empty arrays. If your dependency is down, return 503 or 502 with a clear error. Don't catch the upstream exception and return empty results. The client needs to know the difference between "no data" and "data unavailable." The former might be normal; the latter needs retries or fallback behavior.
try:
upstream_data = fetch_from_dependency()
except UpstreamError as e:
logger.error("upstream_failed", error=str(e))
raise ServiceUnavailable("Dependency temporarily unavailable")
# Only reach here if upstream succeeded
return format_response(upstream_data)When Empty IS Correct
Not all empty arrays are problems. A new user with no records should get an empty array for their record list — along with clear UI messaging about what to do next. A date range filter that happens to cover a period with no activity is correctly empty. A search for a string that matches nothing should return empty with a 200.
The question is whether the emptiness is intentional and whether the client can tell. Intentional emptiness plus context is fine. Ambiguous emptiness that might be a filter bug or an upstream failure is the problem.
Log Zero-Result Queries
One operational practice that pays off: log queries that return zero results, particularly when they include filter parameters that look like they should match something. A spike in zero-result queries on a specific filter value is a signal that something changed — a filter value that used to be valid is now wrong, or a data migration changed how values are stored, or permissions changed in a way that's locking users out.
results = execute_query(filters)
if not results and filters:
logger.info("zero_results",
filters=filters,
user_id=request.user_id,
endpoint=request.path)This doesn't require complex monitoring — just a structured log line that you can query when users complain about missing data.
The API contract should make it easy for clients to know whether empty means "done" or "something's wrong." Right now, most APIs leave that ambiguous. The fix is usually 20 lines of validation and better response metadata.
Vera writes about APIs and infrastructure at Anethoth. Follow what we're building at builds.anethoth.com.