Skill note: I did not use a skill because this is a direct narrative expansion rather than a data‑driven comparison or decision piece.
I still remember the first time a production email rendered as “{0}” instead of a customer name. The bug wasn’t in the template—it was in how the formatting call was made. MessageFormat can be a quiet workhorse in Java internationalization, but it has sharp edges when you call the low-level format(Object[], StringBuffer, FieldPosition) method directly. That method is powerful because it lets you stream formatted output into a buffer and also capture field positions for alignment or later inspection. It’s also easy to misuse if you pass the wrong buffer or misunderstand what FieldPosition actually tracks.
I’ll walk you through how format() really behaves, why you might choose it over higher-level alternatives, and how to read the output and field positions with confidence. I’ll use complete, runnable examples, show the NullPointerException case that trips up many devs, and explain how the pattern syntax affects output. You’ll also get practical guidance on when to reach for MessageFormat in a modern codebase, how to avoid common mistakes, and how to keep performance predictable.
The Mental Model: Pattern + Arguments + Buffer
MessageFormat is a template engine baked into the JDK for locale-aware formatting. When you call:
public final StringBuffer format(Object[] arguments,
StringBuffer result,
FieldPosition pos)
you’re asking Java to take a pattern (already set on the MessageFormat instance), format each argument according to that pattern, and append the output to the provided StringBuffer. The key points I keep in mind are:
- The pattern belongs to the
MessageFormatinstance, not the method call. - The arguments array is indexed from 0, and each placeholder like
{0}or{1, number}maps to those indices. - The
StringBufferyou pass is the output target; if it’s null, you will get aNullPointerException. FieldPositionis only meaningful if you care where a specific formatted field starts and ends.
Here’s a simple analogy I use: MessageFormat is a stencil, the arguments are the paint, and the StringBuffer is the canvas. The FieldPosition is a note that says “the number I care about starts here and ends there.” If you forget the canvas, you can’t paint anything.
Why Use format(Object[], StringBuffer, FieldPosition)?
Most developers are used to MessageFormat.format(String pattern, Object... arguments) or String.format. Those are convenient, but the low-level format(Object[], StringBuffer, FieldPosition) has three advantages that matter in real-world systems:
1) You can append to an existing buffer, which is useful when you are streaming or composing larger messages.
2) You can capture alignment or field positions, which helps when you need to highlight or align output later.
3) You can reuse a MessageFormat instance with a fixed pattern, which avoids repeated parsing of the pattern.
I reach for this method when I’m building localized logs, auditing reports, or structured text output where I need to know exactly where a specific field landed in the output.
Example Set 1: Number Formatting with Pattern Variations
Let’s start with a complete, runnable example that mirrors the common “number formatting in different styles” use case. I use a single Double value and format it three different ways in one pattern. The pattern itself is the core of the example.
import java.text.FieldPosition;
import java.text.MessageFormat;
public class MessageFormatExample1 {
public static void main(String[] args) {
try {
// Pattern formats argument 0 in three different number styles
MessageFormat format = new MessageFormat("{0, number, #}, {0, number, #.##}, {0, number}");
// FieldPosition tracks where the argument appears
FieldPosition position = new FieldPosition(MessageFormat.Field.ARGUMENT);
Object[] arguments = { 9.5678 };
StringBuffer output = new StringBuffer(32);
// Append formatted output into the buffer
format.format(arguments, output, position);
System.out.println("formatted array: " + output);
System.out.println("argument start index: " + position.getBeginIndex());
System.out.println("argument end index: " + position.getEndIndex());
} catch (NullPointerException e) {
System.out.println("Null buffer passed to format(): " + e.getMessage());
}
}
}
What to notice:
- The pattern uses three variations of the same argument: a rounded number with
#, a number with up to two decimals, and the default number format for the locale. FieldPositionwithMessageFormat.Field.ARGUMENTtracks the first occurrence of the argument in the output. If you need positions for later occurrences, you’ll need a different approach (more on that below).- The output buffer is reused, which is good for memory churn if you’re calling this in a loop.
If you run this on a typical US locale, you’ll see something like:
formatted array: 10, 9.57, 9.568
argument start index: 0
argument end index: 2
The first formatted value is “10” because “#” rounds to the nearest integer and drops decimals. That’s a subtle but important detail; if you expected “9” you’re thinking of truncation, not rounding.
Example Set 1, Part 2: The Null Buffer Trap
The method requires a non-null StringBuffer. If you pass null, it throws NullPointerException. This is not a gentle failure, and it’s exactly why I prefer to allocate the buffer right before calling format() instead of accepting a nullable buffer from other parts of the codebase.
import java.text.FieldPosition;
import java.text.MessageFormat;
public class MessageFormatExample2 {
public static void main(String[] args) {
try {
MessageFormat format = new MessageFormat("{0, number, #}, {0, number, #.##}, {0, number}");
FieldPosition position = new FieldPosition(MessageFormat.Field.ARGUMENT);
Object[] arguments = { 9.5678 };
// Intentionally pass null to show the exception
format.format(arguments, null, position);
} catch (NullPointerException e) {
System.out.println("StringBuffer is null: " + e);
}
}
}
This is a great example for code reviews. If you see a nullable buffer, you should either validate before calling format() or create a new buffer when needed. I recommend doing both: validate if your method accepts a buffer, and still ensure you create one if you don’t get it.
Understanding FieldPosition: What It Gives You (and What It Doesn’t)
FieldPosition is often misunderstood. It doesn’t collect positions for every field in the output. It captures the first field that matches the provided field attribute. For MessageFormat, the most common attribute is MessageFormat.Field.ARGUMENT, which marks where the argument was inserted.
If your pattern inserts the same argument multiple times, FieldPosition will still give you the first occurrence only. For example, in the pattern:
"{0, number, #}, {0, number, #.##}, {0, number}"
the FieldPosition for ARGUMENT returns the range for the first formatted value. If you need all positions, you have a few options:
- Parse the output after formatting and locate substrings (not ideal for localization).
- Use
MessageFormat.parseto map back to arguments (expensive and fragile with repeated values). - Build a custom format with explicit separators and then compute offsets as you append.
- Use
formatToCharacterIteratorand inspectAttributedCharacterIteratorfor all occurrences.
In practice, I treat FieldPosition as a “good enough” tool for aligning a single field in a composite output, not as a full indexing system.
When to Use MessageFormat vs Alternatives
I still see teams default to String.format because it’s familiar. But MessageFormat exists for localization and number/date formatting that respects locale. I choose between approaches based on the message type and target audience.
Here’s a quick comparison that I use in reviews. I’m giving you a clear recommendation, not a vague split.
Locale-Aware Formatting
Field Position Support
—
—
Strong
Yes
Strong
No
Weak (locale optional)
No
My recommendation: use MessageFormat for any user-facing text that might be localized, and use the low-level format(Object[], StringBuffer, FieldPosition) when you need control over output buffering or field positions. For debug logs, String.format is fine, but don’t mix it into UI or email templates that might be translated.
Pattern Behavior That Surprises People
MessageFormat’s pattern syntax is compact, but it has surprises. I keep these rules in mind and teach them to teammates:
- Number formats use rounding, not truncation.
#means a digit if needed, not “cut decimals.” - The default number format is locale-specific. A German locale will use commas and periods differently than a US locale.
- Literal braces need escaping by doubling:
‘{‘and‘}‘or quoting with single quotes. - Single quotes are special. To include a single quote, use two:
‘‘.
Here’s a compact example that shows locale sensitivity. You can run this and pass different locales to see the effect:
import java.text.MessageFormat;
import java.util.Locale;
public class MessageFormatLocaleDemo {
public static void main(String[] args) {
MessageFormat us = new MessageFormat("Total: {0, number, #,##0.00}", Locale.US);
MessageFormat de = new MessageFormat("Total: {0, number, #,##0.00}", Locale.GERMANY);
Object[] data = { 1234567.89 };
System.out.println(us.format(data));
System.out.println(de.format(data));
}
}
If your product has international users, this is not optional; it’s the correct behavior. I often see teams using String.format and then struggling with separators for months. MessageFormat solves that.
Common Mistakes and How I Avoid Them
I’ve reviewed a lot of MessageFormat code, and the same mistakes keep showing up. Here’s how I prevent them.
- Passing null
StringBuffer: always create a buffer in the same method that callsformat(). If you accept a buffer parameter, validate it and fail fast. - Forgetting to escape braces or quotes: I keep a small set of “pattern tests” in unit tests. A failing test is cheaper than a broken email.
- Misaligned arguments: I encourage using named constants or enums to document argument indices. That avoids “{2} means price” confusion.
- Re-parsing the pattern on every call: create and reuse
MessageFormatinstances if the pattern is stable. - Confusing
MessageFormatwithString.format:MessageFormatuses{0}, not%s. This mismatch is a frequent source of runtime formatting errors.
Here’s an example of a quick pattern test I add when templates are stable:
import java.text.MessageFormat;
import static org.junit.jupiter.api.Assertions.assertEquals;
class MessageFormatPatternTest {
@org.junit.jupiter.api.Test
void pricePatternFormatsCorrectly() {
MessageFormat format = new MessageFormat("Item {0}: {1, number, #,##0.00}");
Object[] data = { "Notebook", 19.9 };
assertEquals("Item Notebook: 19.90", format.format(data));
}
}
This kind of test guards against silent pattern regressions when someone edits a resource bundle.
Performance and Memory Behavior
MessageFormat is not slow, but it does real work—parsing patterns, formatting numbers and dates, and building output strings. In my experience:
- Formatting a short message with 2–3 arguments typically falls in the 0.05–0.3 ms range on modern JVMs.
- Parsing patterns repeatedly can add noticeable overhead in tight loops. I avoid that by reusing
MessageFormatobjects. - Using an existing
StringBufferreduces allocations and GC pressure if you’re producing many messages in sequence.
When I see MessageFormat inside a hot path, I do three things:
1) Move pattern parsing out of the loop.
2) Reuse a StringBuffer per thread or per method call.
3) Confirm the locale is correct for the user before formatting.
If you do those, MessageFormat is perfectly fine for most systems. If you need extremely high throughput and the output is not user-facing, I still consider a simpler approach, but I don’t replace MessageFormat by default without a clear performance profile.
When I Use It and When I Don’t
I use MessageFormat.format(Object[], StringBuffer, FieldPosition) when:
- I’m building localized UI messages, emails, or PDF reports.
- I need exact field positions to align values in fixed-width text.
- I want one pattern shared across many calls without reparsing.
I avoid it when:
- The message is purely internal and never localized (
String.formatis simpler). - The pattern is dynamic and never reused (
MessageFormatbecomes overhead). - I need complex conditional logic in templates (I’ll use a template engine instead).
That’s a clear decision rule I apply across Java services, and it keeps message formatting consistent and safe.
Practical Edge Cases You Should Test
Before you ship, these are the edge cases I always check in a MessageFormat-heavy module:
- Missing arguments:
MessageFormatinserts “{n}” if argument n is missing. That’s a silent failure you can catch with tests. - Null arguments: The string “null” is inserted. If that’s not acceptable, you should pre-validate.
- Repeated values: The same argument appearing multiple times can be formatted differently in the same pattern. That’s powerful but also easy to misuse.
- Locale changes: A
MessageFormatinstance holds a locale. If you reuse it across users, you must re-create it or set the locale appropriately.
I typically add unit tests for at least missing arguments and locale behavior. It saves a lot of support time.
A Simple, Safe Wrapper I Use in Teams
If you want to reduce misuse, wrap MessageFormat in a small helper. This example ensures the buffer is non-null and keeps pattern parsing stable. It also makes call sites simpler.
import java.text.FieldPosition;
import java.text.MessageFormat;
import java.util.Locale;
public class SafeMessageFormatter {
private final MessageFormat format;
public SafeMessageFormatter(String pattern, Locale locale) {
this.format = new MessageFormat(pattern, locale);
}
public String format(Object[] arguments) {
StringBuffer buffer = new StringBuffer(64);
FieldPosition position = new FieldPosition(MessageFormat.Field.ARGUMENT);
format.format(arguments, buffer, position);
return buffer.toString();
}
}
I use this wrapper in teams where templates are stored in resource bundles and the call sites are all over the codebase. It centralizes pattern creation, guarantees a non-null buffer, and keeps locale decisions consistent. It also makes it easy to add validation hooks later if you want to reject nulls or missing arguments.
Deepening Example Set 1: Variants, Locale, and Alignment
“Set 1” is usually taught as a simple number formatting sample, but I’ve learned that real-world code needs more context. I expand it in three directions: locale control, argument alignment, and mixed types.
1) Locale Control
If you want a specific locale, you should set it at construction time and avoid using the default locale implicitly. I do that explicitly when the output is user-facing.
import java.text.FieldPosition;
import java.text.MessageFormat;
import java.util.Locale;
public class MessageFormatLocaleSet1 {
public static void main(String[] args) {
MessageFormat format = new MessageFormat("{0, number, #}, {0, number, #.##}, {0, number}", Locale.FRANCE);
Object[] arguments = { 9.5678 };
StringBuffer buffer = new StringBuffer(32);
FieldPosition pos = new FieldPosition(MessageFormat.Field.ARGUMENT);
format.format(arguments, buffer, pos);
System.out.println(buffer.toString());
}
}
When I run this, the decimal and grouping separators follow French conventions. That’s not a cosmetic change—it’s a usability change. People interpret numbers differently based on locale.
2) Argument Alignment for Fixed-Width Output
When I need alignment (like a plain-text report), I use a pattern with fixed-width and then use FieldPosition to adjust after the fact if needed.
import java.text.FieldPosition;
import java.text.MessageFormat;
public class MessageFormatAlignedSet1 {
public static void main(String[] args) {
MessageFormat format = new MessageFormat("{0, number, #,##0.00} | {1}");
Object[] arguments = { 1234.5, "Subtotal" };
StringBuffer buffer = new StringBuffer(64);
FieldPosition pos = new FieldPosition(MessageFormat.Field.ARGUMENT);
format.format(arguments, buffer, pos);
System.out.println(buffer.toString());
System.out.println("First field starts at: " + pos.getBeginIndex());
}
}
I don’t always need the position, but when I do, it makes it easy to highlight or underline the numeric portion later.
3) Mixed Types in One Pattern
Set 1 is number‑focused, but real output often mixes text, numbers, and dates. I keep the same argument index pattern and add additional types to show the workflow.
import java.text.FieldPosition;
import java.text.MessageFormat;
import java.util.Date;
public class MessageFormatMixedSet1 {
public static void main(String[] args) {
MessageFormat format = new MessageFormat("On {2, date, long} you bought {1} items. Total: {0, number, #,##0.00}");
Object[] arguments = { 42.5, 3, new Date() };
StringBuffer buffer = new StringBuffer(128);
FieldPosition pos = new FieldPosition(MessageFormat.Field.ARGUMENT);
format.format(arguments, buffer, pos);
System.out.println(buffer.toString());
}
}
I like this example because it shows that you can control which argument uses which formatter, and the ordering in the pattern doesn’t have to match the ordering of the arguments.
FieldPosition vs formatToCharacterIterator
If you truly need all field positions, I skip FieldPosition and move to formatToCharacterIterator. It’s more verbose, but it lets you inspect every argument, every occurrence, and even the specific formatter that was used.
Here’s a compact example that prints all argument positions for a given pattern:
import java.text.AttributedCharacterIterator;
import java.text.MessageFormat;
import java.text.MessageFormat.Field;
import java.util.Map;
public class MessageFormatIteratorDemo {
public static void main(String[] args) {
MessageFormat format = new MessageFormat("{0, number, #}, {0, number, #.##}, {1}");
Object[] data = { 9.5678, "done" };
AttributedCharacterIterator it = format.formatToCharacterIterator(data);
int start = it.getBeginIndex();
int end = it.getEndIndex();
it.first();
while (it.getIndex() < end) {
Map attrs = it.getAttributes();
if (attrs.containsKey(Field.ARGUMENT)) {
int argIndex = (Integer) attrs.get(Field.ARGUMENT);
int runStart = it.getRunStart(Field.ARGUMENT);
int runLimit = it.getRunLimit(Field.ARGUMENT);
System.out.println("argument " + argIndex + " -> " + runStart + ".." + runLimit);
it.setIndex(runLimit);
} else {
it.next();
}
}
}
}
I use this approach for advanced formatting like syntax highlighting, interactive UIs, or exporting annotations alongside the formatted text. It’s more code, but it gives you full control.
Thread Safety and Reuse
One subtle issue: MessageFormat is not thread-safe. This matters when you cache it or store it in a static field. I see this error in shared utilities all the time.
My rule:
- If multiple threads access a
MessageFormatinstance, I wrap it inThreadLocalor create a new instance per request. - If it’s used in a single thread (like a request-scoped service), I reuse it freely.
Here’s a simple thread-safe wrapper:
import java.text.MessageFormat;
import java.util.Locale;
public class ThreadLocalMessageFormat {
private final String pattern;
private final Locale locale;
private final ThreadLocal local;
public ThreadLocalMessageFormat(String pattern, Locale locale) {
this.pattern = pattern;
this.locale = locale;
this.local = ThreadLocal.withInitial(() -> new MessageFormat(pattern, locale));
}
public MessageFormat get() {
return local.get();
}
}
I keep this in mind any time I want to cache a formatter for performance reasons.
Resource Bundles and Real Localization
MessageFormat shines when you pair it with ResourceBundle. I do this in almost every app with localization requirements.
Example:
messages.properties:
order.summary=Order {0} for {1} items totals {2,number,#,##0.00}.
Java:
import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;
public class MessageFormatBundleDemo {
public static void main(String[] args) {
ResourceBundle bundle = ResourceBundle.getBundle("messages", Locale.US);
String pattern = bundle.getString("order.summary");
MessageFormat format = new MessageFormat(pattern, Locale.US);
Object[] data = { "A-1024", 3, 42.5 };
System.out.println(format.format(data));
}
}
I like this setup because translators can change the message ordering or even add formatting types without touching Java code. The format() method still works exactly the same, but your text becomes adaptable.
Real-World Scenario: Invoice Email with Set 1 Ideas
Here’s a realistic scenario where the “Set 1” style of number formatting appears in an email template. I combine currency-style formatting with quantity and date details, and I show how I would build the output with a reusable formatter.
import java.text.FieldPosition;
import java.text.MessageFormat;
import java.util.Date;
import java.util.Locale;
public class InvoiceEmailFormatter {
private final MessageFormat format;
public InvoiceEmailFormatter(Locale locale) {
String pattern = "Hello {0},\n" +
"Your order on {1, date, long} includes {2} items.\n" +
"Subtotal: {3, number, #,##0.00}\n" +
"Tax: {4, number, #,##0.00}\n" +
"Total: {5, number, #,##0.00}\n";
this.format = new MessageFormat(pattern, locale);
}
public String formatEmail(Object[] data) {
StringBuffer buffer = new StringBuffer(256);
FieldPosition pos = new FieldPosition(MessageFormat.Field.ARGUMENT);
format.format(data, buffer, pos);
return buffer.toString();
}
public static void main(String[] args) {
InvoiceEmailFormatter formatter = new InvoiceEmailFormatter(Locale.US);
Object[] data = { "Alex", new Date(), 3, 120.0, 9.6, 129.6 };
System.out.println(formatter.formatEmail(data));
}
}
I’m using the exact same low‑level format() method, but I’m applying it to a real use case. In production, I’d likely place the pattern in a bundle and pass the locale from the user profile, but the structure stays the same.
Handling Missing or Null Arguments Gracefully
One frustration with MessageFormat is how it fails silently: missing arguments are rendered literally as {n} and nulls appear as null. In user‑facing text, that’s not acceptable.
I have two patterns for mitigation:
1) Pre-validate the array length and nulls before formatting.
2) Replace nulls with safe defaults in a wrapper.
Here’s a small helper that replaces nulls with a placeholder:
import java.text.MessageFormat;
import java.util.Arrays;
public class NullSafeFormatter {
private final MessageFormat format;
public NullSafeFormatter(String pattern) {
this.format = new MessageFormat(pattern);
}
public String format(Object[] arguments) {
Object[] safe = Arrays.copyOf(arguments, arguments.length);
for (int i = 0; i < safe.length; i++) {
if (safe[i] == null) {
safe[i] = "(missing)";
}
}
return format.format(safe);
}
}
I don’t overuse this, but it’s a practical safety net for systems where data is incomplete.
Debugging Workflow I Use in Practice
When a formatted message is wrong, I keep my debugging tight and repeatable:
1) Print the raw pattern string.
2) Print the arguments array values and types.
3) Format once with MessageFormat.format(pattern, args) for a quick baseline.
4) Re-run with the low-level format(args, buffer, pos) and inspect pos.
5) If the field position looks wrong, switch to formatToCharacterIterator.
This sequence takes minutes and prevents the classic “it looks right in code but wrong at runtime” chase.
Alternative Approaches and Why I Still Use MessageFormat
Sometimes I’m asked: “Why not use a template engine?” I do use template engines for HTML or multi‑line documents with logic, but I still keep MessageFormat for simple localized strings because:
- It’s built into the JDK and has no dependencies.
- It integrates cleanly with
ResourceBundle. - It handles locale‑aware number and date formatting without extra configuration.
If you need conditional logic, list iteration, or HTML escaping, a template engine is often better. But for short messages, especially in resource bundles, I prefer MessageFormat for simplicity and correctness.
A Focused Comparison: Set 1 Style vs String.format
To keep this grounded, I compare the exact Set 1 numeric formatting in MessageFormat to a rough equivalent in String.format:
MessageFormat:
{0, number, #}, {0, number, #.##}, {0, number}
String.format rough equivalent:
String.format("%.0f, %.2f, %f", 9.5678, 9.5678, 9.5678)
The output might look similar in a US locale, but only the MessageFormat version will auto-adjust separators and formatting rules for the user’s locale without extra work. That’s the reason I stick with it for user-facing content.
Testing Strategy That Scales
When I own a module with a lot of MessageFormat patterns, I set up a small test matrix:
- 2–3 representative locales (e.g., US, Germany, Japan).
- A set of patterns with numbers, dates, and text.
- A small sample of arguments, including edge values (0, null, large numbers).
I don’t test every possible input, but I test the patterns themselves because those are what humans edit. I’ve avoided multiple production bugs by catching a quote mismatch or a missing argument before release.
Expansion Strategy
When I expand a short “Set 1” example into production‑ready guidance, I add depth in these specific places:
- I move from single numbers to mixed types (text, number, date) to mirror real templates.
- I show multiple locales so the value of
MessageFormatis visible. - I include
FieldPositionand then show its limitations so the reader doesn’t over-trust it. - I add reusable wrappers that teach safe calling patterns.
- I make sure every code snippet is runnable and focused on one concept at a time.
This approach keeps the narrative practical instead of abstract. It also keeps the reader anchored in the format(Object[], StringBuffer, FieldPosition) method while still seeing the broader context.
If Relevant to Topic
I keep modern tooling and workflows in mind when I use MessageFormat, even though it’s a classic API:
- I still rely on unit tests as the fastest, cheapest guardrail against broken templates.
- For teams with heavy localization, I sometimes use linting or static analysis to detect bad patterns early.
- I make sure the output stays stable across Java versions by pinning locale tests.
These aren’t flashy upgrades, but they solve real problems in production systems.
Final Takeaways I Want You to Remember
I’ll end with the practical points I always share when teaching this method:
- The low-level
format(Object[], StringBuffer, FieldPosition)is about control: output buffering and position tracking. - Always pass a non-null
StringBufferand treat it as the destination, not just a temporary. FieldPositiontracks only the first matching field; useformatToCharacterIteratorfor full coverage.- Patterns are powerful but easy to break; escape braces and quotes and test your templates.
MessageFormatis the right tool for localized text, especially when you need numeric or date formatting.
If you use these rules, the “Set 1” example becomes more than a classroom sample—it becomes a reliable pattern you can ship in production with confidence.


