description and debugDescription

Master overriding description and debugDescription to make your custom objects readable in NSLog and the debugger, and learn to keep sensitive data out of your logs.

Back in 'NSObject Fundamentals' you got a brief mention that every object has a description method used by NSLog's %@ format specifier. This lesson goes much deeper. You'll override description on your own classes, learn the separate role of debugDescription in the debugger, and — critically — learn why a careless description override is one of the easiest ways to leak secrets (passwords, tokens, personal data) into production logs. This is not just a cosmetic debugging topic; it's a real production-reliability and security concern that every iOS/macOS engineer eventually runs into.

Recap: where description comes from

Every Objective-C object inherits from NSObject, and NSObject declares a method called -description that returns an NSString describing the object. The default implementation just prints the class name and memory address, something like <User: 0x600001234567>. That's the version you saw briefly in 'NSObject Fundamentals' — useful for confirming an object exists and knowing its type, but not very informative.

When you write NSLog(@"%@", someObject), the %@ format specifier tells NSLog: 'call -description on this object and substitute the resulting string here.' This happens automatically for any object — you never call -description yourself in normal code; the formatting machinery calls it for you. The same is true for string interpolation via +stringWithFormat: and for anywhere else %@ appears.

Overriding description in your own classes

Because -description is just a regular instance method inherited from NSObject, you can override it in any custom class to control exactly what gets printed. This is one of the highest-value, lowest-effort things you can do to make your own model objects pleasant to debug. Instead of staring at <User: 0x600001234567>, you can see <User: name=Alice, age=29> at a glance.

The override signature is simple: -(NSString *)description, returning an NSString. Inside, you typically build a string using +stringWithFormat: that includes the class name and the fields that matter most for identifying that particular instance.

• Always return a non-nil NSString from description — returning nil can crash or produce '(null)' output depending on context. • Keep it reasonably short; description is meant for quick identification, not a full data dump. • Include fields that uniquely identify or usefully summarize the instance (an id, a name, a status) rather than every property. • Consider calling [super description] and appending to it if you still want the default class/address prefix.

debugDescription: a distinct, debugger-specific hook

NSObject also declares -debugDescription, a separate method with its own default implementation. The key thing to understand is that debugDescription and description serve different call sites. NSLog and %@ always call -description. The lldb debugger's po (print object) command, however, prefers -debugDescription if it's implemented, and only falls back to -description if debugDescription isn't overridden.

This distinction exists because what's useful to print in a quick log statement (a short, single-line summary) is not always what's most useful while you're paused at a breakpoint actively inspecting an object (where you might want every property, formatted across multiple lines, with more verbose detail).

In practice: override description for a concise, log-friendly summary, and optionally override debugDescription separately for a more verbose, breakpoint-friendly dump. If you only override description, po will use it too — that's fine for simple classes. Reserve a separate debugDescription for classes complex enough to benefit from extra detail only while actively debugging.

How NSLog and po actually resolve which method to call

It helps to think of it as a small decision tree. For NSLog(@"%@", obj) and any %@ formatting: the runtime always calls -description; debugDescription is never consulted here, even if you've overridden it. For po obj in lldb: lldb first checks whether the object's class implements -debugDescription; if it does, lldb calls that; if not, lldb falls back to -description (the same one NSLog would use).

This means a class with only a description override behaves identically whether you NSLog it or po it. A class with both overridden shows a short line in your log output but a richer dump when you po it at a breakpoint — giving you the best of both without contaminating your log files with breakpoint-level verbosity.

The production concern: never log secrets through description

Here is the real-world danger this lesson exists to highlight. Once you override -description on a class, every future NSLog(@"%@", instance) anywhere in your codebase — including code written by teammates who have never seen your class definition — will automatically print whatever your override produces. If that class happens to hold an auth token, password, session cookie, credit card number, or personal data like an email or phone number, and your description naively includes it, you have created a silent, easy-to-trigger data leak that will show up in Xcode console output, crash logs, and — worse — centralized production log aggregators (which are often far less access-controlled than you'd hope).

This is especially insidious because the leak doesn't require anyone to intend it. A well-meaning engineer six months later writes NSLog(@"Session created: %@", session) for an unrelated debugging task, and now the session's bearer token is sitting in a log file, possibly retained for months, possibly readable by a wider set of people than should ever see production credentials.

The fix is straightforward but must be deliberate: when you write a description override, treat it the same way you'd treat an API response or an analytics event — actively decide what is safe to expose, and redact or omit everything else. Print a placeholder like *** or a masked form instead of the real value.

Cross-references: Production Reliability and Documentation

The 'Production Reliability' module covers logging and security practices at a broader, system level — rate limiting log volume, structured logging, log redaction policies, and incident response. This lesson is the object-level complement: the discipline you apply inside a single -description override so that the logging system described there never receives secrets in the first place.

The 'Documentation' module discusses writing comments and documentation that communicate intent to other engineers. A well-written description override is itself a form of documentation — it tells anyone debugging your object, at a glance, what state it's in — so treat the fields you choose to include with the same care you'd give a doc comment: informative, but never exposing more than necessary.

Overriding description and debugDescription

description is used by NSLog/%@ and by po as a fallback. debugDescription, when implemented, is preferred by po in lldb. Even in debugDescription, redact anything sensitive — a breakpoint dump can still end up in a shared screenshot or pasted stack trace.

- (NSString *)description {
    return [NSString stringWithFormat:@"<%@: name=%@>",
            NSStringFromClass([self class]), self.name];
}

- (NSString *)debugDescription {
    return [NSString stringWithFormat:
            @"<%@: 0x%p>\n  name = %@\n  age = %ld\n  email = %@",
            NSStringFromClass([self class]), self,
            self.name, (long)self.age, @"***"];
}

Code Examples

Without any override, NSObject's default -description just prints the class name and the object's memory address. It confirms the object's type and identity but tells you nothing about its actual contents. Expected result: <User: 0x600002a1c3d0> NSLog(@"%@", u);

Now that -description is overridden, the same NSLog call automatically picks up the new implementation. Every place in the codebase that logs a User object benefits from this improvement without any other code needing to change. Expected result: <User: Alice, age 29> return [NSString stringWithFormat:@"<User: %@, age %ld>", self.name, (long)self.age];

po in lldb consults debugDescription first, so it shows the multi-line, more verbose dump — perfect for actively inspecting state at a breakpoint. NSLog still only ever calls description, so log output stays short and consistent. This split lets you keep production/log output terse while still getting rich detail during interactive debugging sessions.

This override looks harmless and even helpful for debugging, but it silently prints a live auth token and a user's email address anywhere Session is logged — including from code the author of Session never wrote or reviewed.

Common mistake: Exposing authToken and userEmail directly means every future NSLog(@"%@", session) call anywhere in the app leaks live credentials and PII into console and production logs.

@interface User : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
@end

@implementation User
@end

User *u = [[User alloc] init];
u.name = @"Alice";
u.age = 29;
NSLog(@"%@", u);
// Output: <User: 0x600002a1c3d0>
@implementation User

- (NSString *)description {
    return [NSString stringWithFormat:@"<User: %@, age %ld>",
            self.name, (long)self.age];
}

@end

NSLog(@"%@", u);
// Output: <User: Alice, age 29>
@implementation User

- (NSString *)description {
    return [NSString stringWithFormat:@"<User: %@>", self.name];
}

- (NSString *)debugDescription {
    return [NSString stringWithFormat:
            @"<User %p>\n  name = %@\n  age  = %ld",
            self, self.name, (long)self.age];
}

@end

// In lldb, paused at a breakpoint:
// (lldb) po u
// <User 0x600002a1c3d0>
//   name = Alice
//   age  = 29
//
// In code:
// NSLog(@"%@", u);
// Output: <User: Alice>
@interface Session : NSObject
@property (nonatomic, copy) NSString *userEmail;
@property (nonatomic, copy) NSString *authToken;
@end

@implementation Session

// DANGEROUS: leaks the real token and email into every log line.
- (NSString *)description {
    return [NSString stringWithFormat:
            @"<Session: email=%@ token=%@>",
            self.userEmail, self.authToken];
}

@end

Why this bites teams in production

Logging frameworks used in shipping apps and backend services routinely capture NSLog output (or its structured-logging equivalent) and forward it to centralized log aggregation systems for crash triage and analytics. Those systems are often accessible to a much wider group of engineers, support staff, or even third-party vendors than the original credential holder ever intended. A description override that includes a raw token or password doesn't need a bug to leak — it just needs someone, someday, to log that object for an unrelated reason. Because this is a silent, structural risk rather than an obvious crash, it's exactly the kind of thing code review should specifically check for whenever a description override touches a model that has auth-, payment-, or PII-related fields.

• (NSString *)description { return [NSString stringWithFormat:@"<Session: email=%@ token=%@>", self.userEmail, self.authToken]; } - (NSString *)description { return [NSString stringWithFormat:@"<Session: email=*, token=*>"]; }

Redact or omit fields that are sensitive — auth tokens, passwords, full email addresses, phone numbers, payment details — from description entirely. If a masked hint is genuinely useful (e.g. confirming which user without exposing the full address), print something like <User email=a***@example.com> rather than the raw value. Treat every description override as a public-facing log format, because effectively it is one.

Which method does NSLog(@"%@", obj) always call? A. -debugDescription B. -description C. -toString D. -stringValue

B. -description — NSLog and the %@ format specifier always call -description, never -debugDescription, regardless of whether the class implements both.

In lldb, which method does po prefer when a class implements both? A. It always uses description, same as NSLog B. It always uses debugDescription C. It uses debugDescription if implemented, otherwise falls back to description D. It picks randomly between the two

C. It uses debugDescription if implemented, otherwise falls back to description — lldb's po command checks for -debugDescription first and uses it if the class implements it; if not, it falls back to -description, the same method NSLog uses.

What is the main production risk of a naive -description override on a class holding an auth token? A. It slows down NSLog performance significantly B. It can silently leak the token into console and centralized production logs anywhere the object is logged C. It causes a compiler warning D. It prevents the object from being archived with NSCoding

B. It can silently leak the token into console and centralized production logs anywhere the object is logged — Once overridden, description runs automatically for every future NSLog call on that type, including calls written by other engineers who never see the class internals — so a raw token in the output becomes a silent, easy-to-trigger leak.

What should a description override do with a sensitive field like a password or full card number? A. Include it in full so debugging is easier B. Base64-encode it before printing C. Redact, mask, or omit it entirely (e.g. print '***' or last-4-digits only) D. Only print sensitive fields in debugDescription, never in description

C. Redact, mask, or omit it entirely (e.g. print '***' or last-4-digits only) — The safe practice is to redact, mask, or omit sensitive values outright — treat every description override as a de facto public logging format, since any code anywhere can trigger it via NSLog.

What happens if a class overrides description but not debugDescription? A. po will crash B. po falls back to using -description, so po and NSLog show the same output C. po prints only the memory address D. The compiler generates a default debugDescription automatically with different content

B. po falls back to using -description, so po and NSLog show the same output — When debugDescription isn't implemented, lldb's po command falls back to calling -description, so both NSLog and po produce identical output for that class.

You have a Payment class with amount, currency, and cardNumber properties. Write a -description override that is useful for debugging (shows amount and currency) but never exposes the full card number.

• (NSString *)description { NSString *last4 = [self.cardNumber substringFromIndex:self.cardNumber.length - 4]; return [NSString stringWithFormat:@"<Payment: %@ %@, card **** %@>", self.amount, self.currency, last4]; }

Print only the last 4 digits of cardNumber, e.g. ** ** 1234, and include amount/currency freely since they aren't sensitive on their own.