Make ToString() return an error instead of panic (#117)

Fixes #100
diff --git a/parser_test.go b/parser_test.go
index 048f514..188876b 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -673,7 +673,10 @@
 		t.Errorf("Test failed to parse: %v", err)
 		return
 	}
-	result := tree.ToString()
+	result, err := tree.ToString()
+	if err != nil {
+		t.Errorf("Unexpected error: %s", err)
+	}
 	expected := "\n[foo]\n\n  [[foo.bar]]\n    a = 42\n\n  [[foo.bar]]\n    a = 69\n"
 	if result != expected {
 		t.Errorf("Expected got '%s', expected '%s'", result, expected)
diff --git a/tomltree_conversions.go b/tomltree_conversions.go
index db3da0d..fc8f22b 100644
--- a/tomltree_conversions.go
+++ b/tomltree_conversions.go
@@ -87,7 +87,7 @@
 	case nil:
 		return ""
 	default:
-		panic(fmt.Sprintf("unsupported value type %T: %v", value, value))
+		panic(fmt.Errorf("unsupported value type %T: %v", value, value))
 	}
 }
 
@@ -154,6 +154,23 @@
 	return strings.Join(resultChunks, "")
 }
 
+// Same as ToToml(), but does not panic and returns an error
+func (t *TomlTree) toTomlSafe(indent, keyspace string) (result string, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			result = ""
+			switch x := r.(type) {
+			case error:
+				err = x
+			default:
+				err = fmt.Errorf("unknown panic: %s", r)
+			}
+		}
+	}()
+	result = t.toToml(indent, keyspace)
+	return
+}
+
 func convertMapStringString(in map[string]string) map[string]interface{} {
 	result := make(map[string]interface{}, len(in))
 	for k, v := range in {
@@ -170,15 +187,18 @@
 	return result
 }
 
-// ToString is an alias for String
-func (t *TomlTree) ToString() string {
-	return t.String()
+// ToString generates a human-readable representation of the current tree.
+// Output spans multiple lines, and is suitable for ingest by a TOML parser.
+// If the conversion cannot be performed, ToString returns a non-nil error.
+func (t *TomlTree) ToString() (string, error) {
+	return t.toTomlSafe("", "")
 }
 
 // String generates a human-readable representation of the current tree.
-// Output spans multiple lines, and is suitable for ingest by a TOML parser
+// Alias of ToString.
 func (t *TomlTree) String() string {
-	return t.toToml("", "")
+	result, _ := t.ToString()
+	return result
 }
 
 // ToMap recursively generates a representation of the current tree using map[string]interface{}.
diff --git a/tomltree_conversions_test.go b/tomltree_conversions_test.go
index af3e9df..40b29b7 100644
--- a/tomltree_conversions_test.go
+++ b/tomltree_conversions_test.go
@@ -1,6 +1,7 @@
 package toml
 
 import (
+	"errors"
 	"reflect"
 	"strings"
 	"testing"
@@ -15,7 +16,8 @@
 		t.Fatal("Unexpected error:", err)
 	}
 
-	reparsedTree, err := Load(toml.ToString())
+	tomlString, _ := toml.ToString()
+	reparsedTree, err := Load(tomlString)
 
 	assertTree(t, reparsedTree, err, map[string]interface{}{
 		"name": map[string]interface{}{
@@ -39,7 +41,7 @@
 		  foo = 1
 		  bar = "baz2"`)
 
-		stringRepr := tree.ToString()
+		stringRepr, _ := tree.ToString()
 
 		t.Log("Intermediate string representation:")
 		t.Log(stringRepr)
@@ -69,6 +71,19 @@
 	}
 }
 
+func TestToStringTypeConversionError(t *testing.T) {
+	tree := TomlTree{
+		values: map[string]interface{}{
+			"thing": []string{"unsupported"},
+		},
+	}
+	_, err := tree.ToString()
+	expected := errors.New("unsupported value type []string: [unsupported]")
+	if err.Error() != expected.Error() {
+		t.Errorf("expecting error %s, but got %s instead", expected, err)
+	}
+}
+
 func TestTomlTreeConversionToMapSimple(t *testing.T) {
 	tree, _ := Load("a = 42\nb = 17")