exception handling
error messages
programming
debugging
software development

Adding information to an exception?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When an exception reaches a log or an error tracker, the original message is often too vague to diagnose the problem quickly. Adding context to an exception is therefore a good practice, but only if you do it in a way that preserves the original error and stack trace. The goal is not to replace the exception with a prettier sentence. The goal is to attach useful state such as input values, operation names, and failure context without hiding the real cause.

Prefer Wrapping Over Replacing

A common mistake is catching an exception and throwing a brand-new one with only a custom message. That destroys the original context unless you chain the original exception as the cause.

Bad pattern:

python
1try:
2    load_user(user_id)
3except Exception:
4    raise RuntimeError("failed to load user")

Better pattern:

python
1try:
2    load_user(user_id)
3except Exception as exc:
4    raise RuntimeError(f"failed to load user {user_id}") from exc

The second version adds the missing domain detail while still preserving the original exception chain.

Python: Add Context Without Losing the Cause

Python gives you several practical options.

For one-off wrapping, raise ... from exc is the cleanest pattern.

python
1import json
2
3
4def parse_config(raw_text: str):
5    try:
6        return json.loads(raw_text)
7    except json.JSONDecodeError as exc:
8        raise ValueError(f"invalid config payload: {raw_text[:40]!r}") from exc

If you need a domain-specific error, define a custom exception type:

python
1class UserImportError(Exception):
2    pass
3
4
5def import_user(row):
6    try:
7        return int(row["age"])
8    except Exception as exc:
9        raise UserImportError(f"invalid user row: {row}") from exc

This keeps the higher-level code expressive without discarding the low-level cause.

C#: Preserve the Inner Exception

C# has the same design rule: if you add a new exception message, include the original exception as InnerException.

csharp
1using System;
2using System.IO;
3
4class Program
5{
6    static void Main()
7    {
8        try
9        {
10            LoadConfig("missing.json");
11        }
12        catch (Exception ex)
13        {
14            Console.WriteLine(ex.Message);
15            Console.WriteLine(ex.InnerException?.Message);
16        }
17    }
18
19    static string LoadConfig(string path)
20    {
21        try
22        {
23            return File.ReadAllText(path);
24        }
25        catch (Exception ex)
26        {
27            throw new InvalidOperationException($"Failed to load config from {path}", ex);
28        }
29    }
30}

Without the ex argument in the constructor, the original failure details are much harder to recover.

Add Context Fields, Not Just Longer Messages

A useful exception message usually answers at least one of these questions:

  • what operation was happening
  • which resource or identifier was involved
  • what input or state made the failure relevant

That means these are often good additions:

  • file path
  • user ID
  • remote endpoint
  • SQL query name, not raw credentials
  • batch number or job ID

You want enough detail to narrow the problem fast, but not so much that the exception becomes a dump of unfiltered state.

Be Careful With Sensitive Data

More context is not always better. Never attach secrets, personal data, or tokens casually just because they might be useful during debugging.

Examples of risky values:

  • passwords
  • full access tokens
  • raw customer PII
  • connection strings with credentials

A good pattern is to include identifiers and safe metadata while redacting sensitive payloads.

python
1try:
2    call_remote_api(token, payload)
3except Exception as exc:
4    raise RuntimeError(f"API call failed for account_id={account_id}") from exc

That keeps the message actionable without leaking secrets.

Logging and Exceptions Are Different Tools

Another important design point: not all diagnostic detail belongs in the exception message itself. Some context belongs in structured logs around the exception.

For example, you may log:

  • request ID
  • trace ID
  • sanitized payload size
  • environment and service name

Then raise or rethrow an exception with a clean, focused message. This balance keeps exception messages readable while still giving operators enough surrounding telemetry.

When Not to Wrap

Do not wrap every exception reflexively. If the current exception type and message already describe the failure well, catching and rethrowing just adds noise.

For example, if a public API already raises a meaningful FileNotFoundError at the right abstraction level, wrapping it in a generic RuntimeError may make things worse rather than better.

Wrap when you are crossing an abstraction boundary and need to explain why a low-level failure matters in your domain.

Common Pitfalls

The biggest mistake is replacing the original exception with a custom one and dropping the cause. That destroys the debugging path.

Another mistake is stuffing huge payloads into exception messages. That makes logs noisy and can expose sensitive data.

Developers also often add context at the wrong layer. If every low-level helper wraps everything, the stack becomes cluttered with repetitive messages.

Finally, do not rely only on exception text when structured logging can carry richer, safer metadata alongside the error.

Summary

  • Add information to exceptions when it helps explain the failure in domain terms.
  • Preserve the original exception with chaining in Python or InnerException in C#.
  • Include useful identifiers and operation context, not uncontrolled dumps of state.
  • Keep secrets and sensitive data out of exception messages.
  • Wrap selectively at abstraction boundaries instead of rethrowing everything blindly.

Course illustration
Course illustration

All Rights Reserved.