The `||` vs `??` Trap That Broke 30% of User Avatars
Problem Scenario
Last Friday, just before the end of the workday, the customer service group chat suddenly exploded:
"Many users' avatars are showing as blank!" "Not all, about 30% of accounts have no avatar"
Checking the frontend logs, the avatar API returned normally, and the url field had a value. Then looking at the rendering layer code:
const avatarUrl = user.avatar?.url || '/default-avatar.png'
The logic seemed fine: use the avatar if it exists, otherwise use the default image. But the strange thing was that users who had avatars were also showing the default image.
Root Cause Analysis
The problem lies in the "aggressive" behavior of the || operator.
|| treats all falsy values as "not having" something. Falsy values in JavaScript include:
| Value | Type |
|---|---|
'' |
Empty string |
0 |
Number zero |
false |
Boolean false |
null |
Null |
undefined |
Undefined |
NaN |
Not a Number |
After investigation, it was found that the url value for these abnormal avatars happened to be an empty string '' — the avatar service returns url: "" in some cases (indicating the user uploaded an avatar but processing failed).
// Actual data
user.avatar = { url: '' } // Empty string, but ≠ no avatar
// Execution logic
const avatarUrl = '' || '/default-avatar.png'
// → Empty string is falsy, swallowed by ||, returns default image ❌
The user had indeed uploaded an avatar (recorded in the database), but because image processing failed and returned an empty string, || forcibly replaced it with the default image — this created the bug where "avatars exist but show the default".
Solution
Option 1: Use ?? (Nullish Coalescing Operator)
const avatarUrl = user.avatar?.url ?? '/default-avatar.png'
?? only takes the right-hand value when the left operand is null or undefined. '', 0, false are all preserved as-is.
Note: ?? cannot be mixed with && or || without parentheses:
// ❌ Syntax error
const a = b ?? c || d
// ✅ Correct
const a = (b ?? c) || d
Option 2: Explicit Check
const avatarUrl = user.avatar?.url && user.avatar.url.length > 0
? user.avatar.url
: '/default-avatar.png'
Safer, but verbose.
Option 3: Backend Unifies Semantics
The backend constrains the url field: no avatar → don't return the field or return null; if an avatar exists (including processing failure), define it properly. The frontend only trusts ??.
Deep Dive: Where Else Are You Likely to Step on This?
Scenario 1: Score/Value 0
// User completed 0 questions
const count = data.completedCount || 'No data'
// 0 is swallowed by ||, displays "No data" ❌
const count = data.completedCount ?? 'No data' // ✅ Displays 0
Scenario 2: CSS Class Concatenation
// The className for a certain state might be an empty string ''
const cls = statusClass || 'default-status'
// Overwritten when empty string ❌
const cls = statusClass ?? 'default-status' // ✅
Scenario 3: Function Default Parameters
function setTheme(color = '#333') { ... }
setTheme('') // Is '' falsy? No, function default parameters only take effect for undefined
// → Passing '' does not trigger the default, color = ''
Function default parameters behave like ??, only taking effect for undefined. Many people mistakenly think they work like ||.
Key Points Summary
| Operator | Condition for Replacement | Use Case |
|---|---|---|
?? |
null / undefined |
Fields that may be missing from backend, optional chaining values |
| ` | ` | |
| Function default params | Only undefined |
Missing parameter values |
One-sentence golden rule:
When you mean "use the default if this data doesn't exist", 99% of the time you should use
??; Only in 1% of cases (like filling in default text when user input is empty) should you deliberately use||.
After fixing this bug, the entire team added a rule to eslint during the retrospective:
// .eslintrc
{
"rules": {
"no-unneeded-ternary": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error"
}
}
And wrote a hard rule into the code review checklist: "Whenever you see || with a variable, first think: should this be ???"
Since then, the blank avatar bug has never reappeared.