diff --git a/src/org/openstreetmap/josm/tools/bugreport/ReportedException.java b/src/org/openstreetmap/josm/tools/bugreport/ReportedException.java
new file mode 100644
index 0000000..4d897af
--- /dev/null
+++ b/src/org/openstreetmap/josm/tools/bugreport/ReportedException.java
@@ -0,0 +1,289 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.bugreport;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.openstreetmap.josm.Main;
+
+/**
+ * This is a special exception that cannot be directly thrown.
+ * <p>
+ * It is used to capture more information about an exception that was already thrown.
+ *
+ * @author Michael Zangl
+ * @see BugReport
+ * @since xxx
+ */
+public class ReportedException extends RuntimeException {
+    private static final int MAX_COLLECTION_ENTRIES = 30;
+    /**
+     *
+     */
+    private static final long serialVersionUID = 737333873766201033L;
+    /**
+     * We capture all stack traces on exception creation. This allows us to trace synchonization problems better. We cannot be really sure what
+     * happened but we at least see which threads
+     */
+    private final transient Map<Thread, StackTraceElement[]> allStackTraces;
+    private final LinkedList<Section> sections = new LinkedList<>();
+    private final transient Thread caughtOnThread;
+    private final Throwable exception;
+    private String methodWarningFrom;
+
+    ReportedException(Throwable exception) {
+        this(exception, Thread.currentThread());
+    }
+
+    ReportedException(Throwable exception, Thread caughtOnThread) {
+        super(exception);
+        this.exception = exception;
+
+        allStackTraces = Thread.getAllStackTraces();
+        this.caughtOnThread = caughtOnThread;
+    }
+
+    /**
+     * Displays a warning for this exception. The program can then continue normally. Does not block.
+     */
+    public void warn() {
+        methodWarningFrom = BugReport.getCallingMethod(2);
+        // TODO: Open the dialog.
+    }
+
+    /**
+     * Starts a new debug data section. This normally does not need to be called manually.
+     *
+     * @param sectionName
+     *            The section name.
+     */
+    public void startSection(String sectionName) {
+        sections.add(new Section(sectionName));
+    }
+
+    /**
+     * Prints the captured data of this report to a {@link PrintWriter}.
+     *
+     * @param out
+     *            The writer to print to.
+     */
+    public void printReportDataTo(PrintWriter out) {
+        out.println("=== REPORTED CRASH DATA ===");
+        for (Section s : sections) {
+            s.printSection(out);
+            out.println();
+        }
+
+        if (methodWarningFrom != null) {
+            out.println("Warning issued by: " + methodWarningFrom);
+            out.println();
+        }
+    }
+
+
+    /**
+     * Prints the stack trace of this report to a {@link PrintWriter}.
+     *
+     * @param out
+     *            The writer to print to.
+     */
+    public void printReportStackTo(PrintWriter out) {
+        out.println("=== STACK TRACE ===");
+        out.println(niceThreadName(caughtOnThread));
+        getCause().printStackTrace(out);
+        out.println();
+    }
+
+
+    /**
+     * Prints the stack traces for other threads of this report to a {@link PrintWriter}.
+     *
+     * @param out
+     *            The writer to print to.
+     */
+    public void printReportThreadsTo(PrintWriter out) {
+        out.println("=== RUNNING THREADS ===");
+        for (Entry<Thread, StackTraceElement[]> thread : allStackTraces.entrySet()) {
+            out.println(niceThreadName(thread.getKey()));
+            if (caughtOnThread.equals(thread.getKey())) {
+                out.println("Stacktrace see above.");
+            } else {
+                for (StackTraceElement e : thread.getValue()) {
+                    out.println(e);
+                }
+            }
+            out.println();
+        }
+    }
+
+    private static String niceThreadName(Thread thread) {
+        String name = "Thread: " + thread.getName() + " (" + thread.getId() + ")";
+        ThreadGroup threadGroup = thread.getThreadGroup();
+        if (threadGroup != null) {
+            name += " of " + threadGroup.getName();
+        }
+        return name;
+    }
+
+    /**
+     * Checks if this exception is considered the same as an other exception. This is the case if both have the same cause and message.
+     *
+     * @param e
+     *            The exception to check against.
+     * @return <code>true</code> if they are considered the same.
+     */
+    public boolean isSame(ReportedException e) {
+        if (!getMessage().equals(e.getMessage())) {
+            return false;
+        }
+
+        Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
+        return hasSameStackTrace(dejaVu, this.exception, e.exception);
+    }
+
+    private static boolean hasSameStackTrace(Set<Throwable> dejaVu, Throwable e1, Throwable e2) {
+        if (dejaVu.contains(e1)) {
+            // cycle. If it was the same until here, we assume both have that cycle.
+            return true;
+        }
+        dejaVu.add(e1);
+
+        StackTraceElement[] t1 = e1.getStackTrace();
+        StackTraceElement[] t2 = e2.getStackTrace();
+
+        if (!Arrays.equals(t1, t2)) {
+            return false;
+        }
+
+        Throwable c1 = e1.getCause();
+        Throwable c2 = e2.getCause();
+        if ((c1 == null) != (c2 == null)) {
+            return false;
+        } else if (c1 != null) {
+            return hasSameStackTrace(dejaVu, c1, c2);
+        } else {
+            return true;
+        }
+    }
+
+    /**
+     * Adds some debug values to this exception.
+     *
+     * @param key
+     *            The key to add this for. Does not need to be unique but it would be nice.
+     * @param value
+     *            The value.
+     * @return This exception for easy chaining.
+     */
+    public ReportedException put(String key, Object value) {
+        String string;
+        try {
+            if (value == null) {
+                string = "null";
+            } else if (value instanceof Collection) {
+                string = makeCollectionNice((Collection<?>) value);
+            } else if (value.getClass().isArray()) {
+                string = makeCollectionNice(Arrays.asList(value));
+            } else {
+                string = value.toString();
+            }
+        } catch (RuntimeException t) {
+            Main.warn(t);
+            string = "<Error calling toString()>";
+        }
+        sections.getLast().put(key, string);
+        return this;
+    }
+
+    private static String makeCollectionNice(Collection<?> value) {
+        int lines = 0;
+        StringBuilder str = new StringBuilder();
+        for (Object e : value) {
+            str.append("\n    - ");
+            if (lines <= MAX_COLLECTION_ENTRIES) {
+                str.append(e);
+            } else {
+                str.append("\n    ... (");
+                str.append(value.size());
+                str.append(" entries)");
+                break;
+            }
+        }
+        return str.toString();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("CrashReportedException [on thread ");
+        builder.append(caughtOnThread);
+        builder.append("]");
+        return builder.toString();
+    }
+
+    private static class SectionEntry {
+        private final String key;
+        private final String value;
+
+        SectionEntry(String key, String value) {
+            this.key = key;
+            this.value = value;
+
+        }
+
+        /**
+         * Prints this entry to the output stream in a line.
+         * @param out The stream to print to.
+         */
+        public void print(PrintWriter out) {
+            out.print(" - ");
+            out.print(key);
+            out.print(": ");
+            out.println(value);
+        }
+    }
+
+    private static class Section {
+
+        private String sectionName;
+        private ArrayList<SectionEntry> entries = new ArrayList<>();
+
+        Section(String sectionName) {
+            this.sectionName = sectionName;
+        }
+
+        /**
+         * Add a key/value entry to this section.
+         * @param key The key. Need not be unique.
+         * @param value The value.
+         */
+        public void put(String key, String value) {
+            entries.add(new SectionEntry(key, value));
+        }
+
+        /**
+         * Prints this section to the output stream.
+         * @param out The stream to print to.
+         */
+        public void printSection(PrintWriter out) {
+            out.println(sectionName + ":");
+            if (entries.isEmpty()) {
+                out.println("No data collected.");
+            } else {
+                for (SectionEntry e : entries) {
+                    e.print(out);
+                }
+            }
+        }
+
+    }
+
+}
diff --git a/test/unit/org/openstreetmap/josm/tools/bugreport/BugReportTest.java b/test/unit/org/openstreetmap/josm/tools/bugreport/BugReportTest.java
new file mode 100644
index 0000000..4ac4e45
--- /dev/null
+++ b/test/unit/org/openstreetmap/josm/tools/bugreport/BugReportTest.java
@@ -0,0 +1,28 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.bugreport;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+/**
+ * Tests the bug report class.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class BugReportTest {
+
+    /**
+     * Test {@link BugReport#getCallingMethod(int)}
+     */
+    @Test
+    public void testGetCallingMethod() {
+        assertEquals("BugReportTest#testGetCallingMethod", BugReport.getCallingMethod(1));
+        assertEquals("BugReportTest#testGetCallingMethod", testGetCallingMethod2());
+    }
+
+    private String testGetCallingMethod2() {
+        return BugReport.getCallingMethod(2);
+    }
+
+}
diff --git a/test/unit/org/openstreetmap/josm/tools/bugreport/ReportedExceptionTest.java b/test/unit/org/openstreetmap/josm/tools/bugreport/ReportedExceptionTest.java
new file mode 100644
index 0000000..4346b10
--- /dev/null
+++ b/test/unit/org/openstreetmap/josm/tools/bugreport/ReportedExceptionTest.java
@@ -0,0 +1,112 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools.bugreport;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+
+import org.junit.Test;
+
+/**
+ * Tests the {@link ReportedException} class.
+ * @author Michael Zangl
+ * @since xxx
+ */
+public class ReportedExceptionTest {
+    private final class CauseOverwriteException extends RuntimeException {
+        private Throwable myCause;
+
+        private CauseOverwriteException(String message) {
+            super(message);
+        }
+
+        @Override
+        public synchronized Throwable getCause() {
+            return myCause;
+        }
+    }
+
+    /**
+     * Tests that {@link ReportedException#put(String, Object)} handles null values
+     */
+    @Test
+    public void testPutDoesHandleNull() {
+        ReportedException e = new ReportedException(new RuntimeException());
+        e.startSection("test");
+        Object[] a = new Object[] {
+                new Object(), null };
+        e.put("testObject", null);
+        e.put("testArray", a);
+        e.put("testList", Arrays.asList(a));
+    }
+
+    /**
+     * Tests that {@link ReportedException#put(String, Object)} handles exceptions during toString fine.
+     */
+    @Test
+    public void testPutDoesNotThrow() {
+        ReportedException e = new ReportedException(new RuntimeException());
+        e.startSection("test");
+        Object o = new Object() {
+            @Override
+            public String toString() {
+                throw new IllegalArgumentException("");
+            }
+        };
+        Object[] a = new Object[] {
+                new Object(), o };
+        e.put("testObject", o);
+        e.put("testArray", a);
+        e.put("testList", Arrays.asList(a));
+    }
+
+    /**
+     * Tests that {@link ReportedException#isSame(ReportedException)} works as expected.
+     */
+    @Test
+    public void testIsSame() {
+        // Do not break this line! All exceptions need to be created in the same line.
+        // CHECKSTYLE.OFF: LineLength
+        // @formatter:off
+        ReportedException[] testExceptions = new ReportedException[] {
+                /* 0 */ genException1(), /* 1, same as 0 */ genException1(), /* 2 */ genException2("x"), /* 3, same as 2 */ genException2("x"), /* 4, has different message than 2 */ genException2("y"), /* 5, has different stack trace than 2 */ genException3("x"), /* 6 */ genException4(true), /* 7, has different cause than 6 */ genException4(false), /* 8, has a cycle and should not crash */ genExceptionCycle() };
+        // @formatter:on
+        // CHECKSTYLE.ON: LineLength
+
+        for (int i = 0; i < testExceptions.length; i++) {
+            for (int j = 0; j < testExceptions.length; j++) {
+                boolean is01 = (i == 0 || i == 1) && (j == 0 || j == 1);
+                boolean is23 = (i == 2 || i == 3) && (j == 2 || j == 3);
+                assertEquals(i + ", " + j, is01 || is23 || i == j, testExceptions[i].isSame(testExceptions[j]));
+            }
+        }
+    }
+
+    private ReportedException genException1() {
+        RuntimeException e = new RuntimeException();
+        return BugReport.intercept(e);
+    }
+
+    private ReportedException genException2(String message) {
+        RuntimeException e = new RuntimeException(message);
+        RuntimeException e2 = new RuntimeException(e);
+        return BugReport.intercept(e2);
+    }
+
+    private ReportedException genException3(String message) {
+        return genException2(message);
+    }
+
+    private ReportedException genException4(boolean addCause) {
+        RuntimeException e = new RuntimeException("x");
+        RuntimeException e2 = new RuntimeException("x", addCause ? e : null);
+        return BugReport.intercept(e2);
+    }
+
+    private ReportedException genExceptionCycle() {
+        CauseOverwriteException e = new CauseOverwriteException("x");
+        RuntimeException e2 = new RuntimeException("x", e);
+        e.myCause = e2;
+        return BugReport.intercept(e2);
+    }
+}
