Prologue: The Empty Canvas
Imagine standing before a pristine canvas—your function signature. You’re designing an API that must handle both presence and absence with grace. The parameters you receive might be fully formed objects, partially complete sketches, or complete voids. How do you define what happens when something isn’t there?
This is the story of two different kinds of nothingness: null and undefined. One is a deliberate statement of absence, the other is the absence of a statement. And in the world of default parameters, this distinction becomes art.
Act I: The Genesis - Before Default Parameters
Let me take you back to a time before ES6, when we painted our defaults by hand:
function createUserProfile(config) {
// The old ways - manual defaulting
confi...
Prologue: The Empty Canvas
Imagine standing before a pristine canvas—your function signature. You’re designing an API that must handle both presence and absence with grace. The parameters you receive might be fully formed objects, partially complete sketches, or complete voids. How do you define what happens when something isn’t there?
This is the story of two different kinds of nothingness: null and undefined. One is a deliberate statement of absence, the other is the absence of a statement. And in the world of default parameters, this distinction becomes art.
Act I: The Genesis - Before Default Parameters
Let me take you back to a time before ES6, when we painted our defaults by hand:
function createUserProfile(config) {
// The old ways - manual defaulting
config = config || {};
const name = config.name || 'Anonymous';
const email = config.email || 'no-email@example.com';
const preferences = config.preferences || {};
return { name, email, preferences };
}
// The problems started here...
createUserProfile({ name: null, email: '' });
// Returns: { name: 'Anonymous', email: 'no-email@example.com' }
// Wait, null became 'Anonymous'? An empty string became our default?
We were using logical OR (||) as a blunt instrument, unable to distinguish between undefined, null, false, 0, and ''. Our subtle intentions were lost in translation.
Act II: The Renaissance - Default Parameters Arrive
Then came ES6, offering us surgical precision:
function createUserProfile({
name = 'Anonymous',
email = 'no-email@example.com',
preferences = {}
} = {}) {
return { name, email, preferences };
}
But here lies our first philosophical choice: what happens when we want to explicitly pass “nothing”?
Act III: The Two Faces of Nothingness
Let me introduce our two protagonists:
// undefined - The unstated absence
let userConfig; // undefined - no value was ever assigned
const response = await getUser(); // undefined - the function returned nothing
// null - The stated absence
const explicitEmpty = null; // "I explicitly want this to be empty"
const userPreference = getUserPreference() || null; // "No preference exists"
In default parameters, they dance differently:
function createDashboard({
theme = 'dark',
layout = 'grid',
widgets = []
} = {}) {
return { theme, layout, widgets };
}
// How they behave:
createDashboard({ theme: undefined });
// theme: 'dark' (defaults because undefined triggers the default)
createDashboard({ theme: null });
// theme: null (null is explicit, so default is not used)
Act IV: The Art of Intentional Design
Now, let’s paint with both brushes intentionally. Consider an API configuration:
// Approach 1: undefined means "use default"
function createAPIClient({
baseURL = 'https://api.example.com',
timeout = 5000,
retries = 3,
cache = null // null means "explicitly no caching"
} = {}) {
const config = {
baseURL,
timeout,
retries,
cache: cache === null ? undefined : cache
};
return axios.create(config);
}
// Usage tells a story:
const client = createAPIClient({
baseURL: undefined, // "I don't care, use default"
timeout: 10000, // "I explicitly want 10 seconds"
retries: null, // "I explicitly want no retries"
cache: undefined // "Let axios decide about caching"
});
Here, we’ve created a language of intention:
undefined→ “I defer to your wisdom”null→ “I explicitly want nothing here”- Any value → “This is my specific request”
Act V: The Deep Dive - Real-World Composition
Let me show you how this plays out in a complex, real-world scenario:
class DocumentProcessor {
// Using null as explicit "not configured"
processDocument(content, {
format = 'markdown',
compression = null, // null: no compression unless detected
metadata = undefined, // undefined: generate default metadata
transformations = [] // empty array: no transformations by default
} = {}) {
const actualMetadata = metadata === undefined
? this.generateDefaultMetadata(content)
: metadata;
const actualCompression = compression === null
? this.detectCompression(content)
: compression;
return {
format,
compression: actualCompression,
metadata: actualMetadata,
transformations,
content: this.transformContent(content, transformations)
};
}
detectCompression(content) {
// Auto-detect gzip, brotli, etc.
return content.startsWith('\x1f\x8b') ? 'gzip' : 'none';
}
}
// The artistry in usage:
const processor = new DocumentProcessor();
// Case 1: Fully automatic
processor.processDocument('# Hello World');
// format: 'markdown' (default)
// compression: auto-detected
// metadata: generated
// transformations: [] (explicit empty array)
// Case 2: Explicit control
processor.processDocument('# Hello World', {
format: 'html',
compression: null, // "Please auto-detect for me"
metadata: null, // "I explicitly want no metadata"
transformations: undefined // "Use your default (empty array)"
});
Act VI: The Nuances - When Philosophy Meets Practice
But the art isn’t just in the creation—it’s in the consumption. Let’s consider function overloads:
// Sometimes we want multiple levels of "defaultness"
function createLogger(config = {}) {
const {
// Level 1: Use default if undefined
level = 'info',
// Level 2: Special handling for null vs undefined
transport = undefined,
// Level 3: Complex default logic
format = null
} = config;
const actualTransport = transport === undefined
? new ConsoleTransport()
: transport;
const actualFormat = format === null
? this.createDefaultFormat(level)
: format;
return new Logger(actualTransport, actualFormat, level);
}
// The symphony of calls:
createLogger();
// Everything defaults
createLogger({ level: null });
// level: null (explicitly no level? Probably not what we want!)
createLogger({ level: undefined });
// level: 'info' (use default)
Here we see the danger: null might not be semantically meaningful for all parameters.
Act VII: The Pattern Library - Recipes for Elegance
After years of painting with these tools, I’ve collected some patterns:
// PATTERN 1: The Explicit Null Pattern
function createResource(options = {}) {
const defaults = {
cache: null, // null: caching decision required
validate: undefined, // undefined: use default validation
timeout: 0 // 0: no timeout by default
};
const config = { ...defaults, ...options };
// Resolve null to meaningful defaults
if (config.cache === null) {
config.cache = process.env.NODE_ENV === 'production';
}
return config;
}
// PATTERN 2: The Three-Tier Default
function withTieredDefaults({
required, // No default - must be provided
optional = 'defaultValue', // Simple default
complex = undefined // Complex default logic needed
} = {}) {
if (required === undefined) {
throw new Error('required parameter missing');
}
const actualComplex = complex === undefined
? computeComplexDefault(required)
: complex;
return { required, optional, complex: actualComplex };
}
// PATTERN 3: The Configuration Builder
class ConfigBuilder {
constructor() {
this.settings = {
// Use undefined for "not yet configured"
database: undefined,
cache: undefined,
logging: undefined
};
}
withDatabase(config = null) {
// null means "explicitly no database"
this.settings.database = config;
return this;
}
build() {
// Resolve undefined to defaults, preserve null as explicit
return {
database: this.settings.database === undefined
? this.getDefaultDatabaseConfig()
: this.settings.database,
// ... similar for other settings
};
}
}
Act VIII: The TypeScript Sonata
For those of us working in TypeScript, the dance becomes even more precise:
interface ApiConfig {
baseURL?: string | null; // Optional: string, null, or undefined
timeout?: number; // Optional: number or undefined
retries: number | null; // Required: must be number or explicit null
}
function createAPI(config: ApiConfig) {
const {
baseURL = 'https://api.example.com',
timeout = 5000,
retries // No default - must be provided per interface
} = config;
// Now we have type-safe intentional absence
return {
baseURL: baseURL === null ? undefined : baseURL,
timeout,
retries: retries === null ? 0 : retries
};
}
TypeScript forces us to be explicit about our nothingness, turning philosophical choices into compiler-enforced contracts.
Act IX: The Wisdom - Knowing When to Use Which
Through many code reviews and production issues, I’ve developed this guidance:
Use undefined for default parameters when:
- You want the parameter to be truly optional
- The caller shouldn’t need to think about it
- You’re providing a sensible default
Use null in default parameters when:
- You need to distinguish between “not provided” and “explicitly empty”
- The parameter represents a meaningful absence
- You’re building APIs that need explicit control
Avoid default parameters when:
- The logic is too complex for simple defaulting
- You need validation beyond presence/absence
- The parameter is required for function operation
Epilogue: The Philosophy of Absence
What started as a technical question about parameter defaults reveals a deeper truth about API design: how we handle absence says as much about our design philosophy as how we handle presence.
undefined is the art of gentle guidance—steering users toward good defaults without restricting their freedom. null is the art of explicit statement—giving users the vocabulary to say “I specifically want nothing here.”
In your next API design, consider what story you want to tell about absence. Do you want to be a gentle guide, offering sensible paths when users are uncertain? Or do you want to be a precise instrument, giving users exact control over every detail?
The beauty is that JavaScript gives us both brushes. The art is in knowing when to use each to paint the masterpiece that is your API.
“In the attitude of silence the soul finds the path in a clearer light, and what is elusive and deceptive resolves itself into crystal clearness.” - Mahatma Gandhi
Sometimes, the most powerful statements are about what we choose not to say.