Be Careful What You Log — It Could Crash Your App
If you are using Timber
to log in your Android app you should be careful about logging variables you don’t fully control. If the input is formatted in a certain way, your app can crash.
One of these will log a URL quite happily, while the other could crash your app at runtime.

Overview
When using Timber
and Kotlin
, you can using Kotlin String Templates to inject variables into your log statements or you can use the standard Timber parameter formatting.
Timber’s Approach
Timber.w("#1 - The URL is %s", url)
Kotlin’s Approach
Timber.w("#2 - The URL is $url")
⚠️ You are advised to use only the Timber parameter formatting style.
When you try to mix both styles things can go wrong, as is detailed below. But even if you keep your style consistent the Timber approach has an advantage; lazy string concatenation. Timber will only build your parameters into a string for logging if the log will be used (if there is a logging tree configured).
If you use the Kotlin string template approach, those strings will always be built even if logging isn’t enabled for that particular build of your app.
Logging URLs
I added some debug-only log statements to log the URLs a WebView
was visiting to help track down a problem I was facing. I visited a few sites and all was fine. And then I visited another site and my app crashed. 😱
In my examples below, I use Timber
, however, the problem really happens in the calls to String.format(String message, Object... args)
.
Examples
In all of the below examples, I am using the same URL:
example.com/%2F
Although this looks contrived, it’s just a simplified example of one I saw in the wild. %2F
is the code for a URL-encoded /
(forward slash).
Ultimately, this is the problem. %2F
itself is a string format placeholder. This is using an explicit index to specify that you will provide a float in the second parameter.
All 6 of these statements look like they will work at first glance. However, one will crash; one will work even thoughLint
will show it as an error; leaving the other 4 to work as expected.

#1— This is ok
Timber.w("#1 - The URL is %s", url)
This is fine; using the standard Timber
mechanism for formatting strings.
#2 — This is ok
Timber.w("#2 - The URL is $url")
This is using Kotlin’s String Templates
which will handle replacing the $url
with the variable. This works as expected; nothing to see here.
#3 — This is ok
Timber.w(“#3 — The URL is %s — visited %d times”, url, count)
We’re providing two variables here, one string and one int and providing both in the standard Timber
manner.
#4 — This is ok
Timber.w(“#4 — The URL is $url — visited $count times”)
As above, two parameters. This time we’re using Kotlin string templates to provide the substitutions.
#5 — This is a bit funny
Timber.w(“#5 — The URL is %s — visited $count times”, url)

We’re providing two parameters again, but this time mixing the style between standard Timber
and Kotlin String Templates
. Things are starting to go a bit off here. The lint check is giving me an error here that I’ve provided the wrong number of arguments. But there is only one %s
and one argument is provided so there should be no lint error here at all.
Despite the red squigglies, this runs fine. By the time it gets to the String.format
method, the message
parameter is
#5 — The URL is %s — visited 4 times
Here, the count
value has already been substituted, but the URL has not.
#6 — And here’s the crash
Timber.w(“#6 — The URL is $url — visited %d times”, count)
Same as #5 above, except we’ve switch it so that $url
is provided as a Kotlin String Template and %d
is provided using the standard Timber arguments for passing an int.
This time, lint is perfectly happy with this line but it crashes when given our URL example.com/%2F
.
This time, when it gets to the String.format
line, the message
is
#6 — The URL is http://example.com/%2F — visited %d times
The URL has been substituted in place but the count has not. This now means it expects two parameters to be provided as arguments to the format method: %2F
and %d
. However, we only satisfy one of those, and therefore it throws an exception:
java.util.UnknownFormatConversionException: Conversion = 'F'
at java.util.Formatter$FormatSpecifier.conversion(Formatter.java:2781)
at java.util.Formatter$FormatSpecifier.<init>(Formatter.java:2811)
at java.util.Formatter$FormatSpecifierParser.<init>(Formatter.java:2624)
at java.util.Formatter.parse(Formatter.java:2557)
at java.util.Formatter.format(Formatter.java:2504)
at java.util.Formatter.format(Formatter.java:2458)
at java.lang.String.format(String.java:2770)
at timber.log.Timber$Tree.formatMessage(Timber.java:561)
at timber.log.Timber$Tree.prepareLog(Timber.java:547)
at timber.log.Timber$Tree.w(Timber.java:457)
at timber.log.Timber$1.w(Timber.java:296)
at timber.log.Timber.w(Timber.java:68)
Summary
It’s fine to use Timber
and Kotlin
together. And while you can technically use either Kotlin string templates or Timber’s standard formatting, use of Timber’s method of providing parameters for logging is preferred as it is more efficient when logging is disabled.
Even if the performance benefit of using Timber’s approach doesn’t bother you and you prefer Kotlin’s, you should never try to mix and match using both approaches.
Miscellaneous
This is questionable, but won’t crash
Timber.i("%2f")
Even though Timber
will helpfully warn you that you haven’t provided the right number of arguments for the given format, nothing will crash. Nothing will crash because Timber
will know there are no arguments, and therefore won’t call through to the formatMessage
method.
if (args != null && args.length > 0) {
message = formatMessage(message, args);
}