Scala/Java: How to convert a stack trace to a string for printing with a logger

As a quick note, I just got a little bit better about how to log stack traces when writing Java or Scala code. This tutorial shows the best way I know to log exceptions. I’ll use Scala code here, but it converts easily to Java.

In Scala I used to get the text from a stack trace and then log it like this:

// this works, but it's not too useful/readable
logger.error(exception.getStackTrace.mkString("\n"))

In that code, getStackTrace returns a sequence, which I convert to a String before printing it.

The best way to format and log stack traces

But recently I learned the following technique, which does a much better job of keeping the formatting when getting the text from a stack trace, and then writing it to a file with a logger:

// scala
val sw = new StringWriter
exception.printStackTrace(new PrintWriter(sw))
logger.error(sw.toString)

I wrote that code in Scala, but as you can see, it converts to Java very easily. In fact, here’s the Java version of the code, which I just used to log exceptions in Android:

// java
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
Log.e(TAG, sw.toString());

This new approach works really well at keeping the stack trace formatting, as I’ll show in the next example.

As I note at the end of this article, you don’t need to use this technique when writing to STDERR. I just use this approach when creating a string to work with a logger.

A stack trace example program

The output from this new approach is much more readable, as I verified with this little test program:

package com.devdaily.sarah.tests

import scala.util.{Try, Success, Failure}
import java.io._

object TrySuccessFailure extends App {

    badAdder(3) match {
        case Success(i) => println(s"success, i = $i")
        case Failure(t) =>
            // this works, but it's not too useful/readable
            //println(t.getStackTrace.mkString("\n"))

            // this works much better
            val sw = new StringWriter
            t.printStackTrace(new PrintWriter(sw))
            println(sw.toString)
    }
   
    /**
     * This method returns a `Success[Int]` if everything goes well,
     * otherwise it throws an exception wrapped in a `Failure`.
     */
    def badAdder(a: Int): Try[Int] = {
        Try({
            val b = a + 1
            if (b == 3) b else {
                val ioe = new IOException("Boom!")
                throw new AlsException("Bummer!", ioe)
            }
        })
    }
   
    class AlsException(s: String, e: Exception) extends Exception(s: String, e: Exception)
}

In that code I intentionally wrap an IOException inside a custom exception, which I then throw from my method.

The StringWriter code prints the following output, which is very readable as stack traces go:

com.devdaily.sarah.tests.TrySuccessFailure$AlsException: Bummer!
    at com.devdaily.sarah.tests.TrySuccessFailure$$anonfun$badAdder$1.apply$mcI$sp(TrySuccessFailure.scala:31)
    at com.devdaily.sarah.tests.TrySuccessFailure$$anonfun$badAdder$1.apply(TrySuccessFailure.scala:27)
    at com.devdaily.sarah.tests.TrySuccessFailure$$anonfun$badAdder$1.apply(TrySuccessFailure.scala:27)
    at scala.util.Try$.apply(Try.scala:161)
    at com.devdaily.sarah.tests.TrySuccessFailure$.badAdder(TrySuccessFailure.scala:27)
    at com.devdaily.sarah.tests.TrySuccessFailure$delayedInit$body.apply(TrySuccessFailure.scala:8)
    at scala.Function0$class.apply$mcV$sp(Function0.scala:40)
    at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:12)
    at scala.App$$anonfun$main$1.apply(App.scala:71)
    at scala.App$$anonfun$main$1.apply(App.scala:71)
    at scala.collection.immutable.List.foreach(List.scala:318)
    at scala.collection.generic.TraversableForwarder$class.foreach(TraversableForwarder.scala:32)
    at scala.App$class.main(App.scala:71)
    at com.devdaily.sarah.tests.TrySuccessFailure$.main(TrySuccessFailure.scala:6)
    at com.devdaily.sarah.tests.TrySuccessFailure.main(TrySuccessFailure.scala)

Caused by: java.io.IOException: Boom!
    at com.devdaily.sarah.tests.TrySuccessFailure$$anonfun$badAdder$1.apply$mcI$sp(TrySuccessFailure.scala:30)

Summary

To be clear, you don’t need this approach when using things like System.out.println or System.err.println; you can just use e.printStackTrace there, which prints to STDERR. But when you want to write to a logger, or otherwise convert a stack trace to a string, the second approach works very well. Here’s the approach encapsulated in a Scala method:

def getStackTraceAsString(t: Throwable) = {
    val sw = new StringWriter
    t.printStackTrace(new PrintWriter(sw))
    sw.toString
}