A story
Back in 2017 I implemented error reporting for a client. Exceptions were sent to Rollbar and could be fixed quickly. My client and I were happy with this integration.
But after a while ago rather odd exceptions popped up. They were similar to this:
|
|
Here’s what I thought:
Why is IntelliJ reporting this? It’s clearly some internal IntelliJ stuff. Probably, this doOkAction() method is buggy. It really does not come from this plugin!
Errors of this kind did not stop, though. But my own plugins were still receiving stack traces as usual. So, I assumed that all of this is not my fault and that I couldn’t fix this in the plugin.
Recently, I implemented error reporting for the upcoming BashSupport Pro. I decided to use a self-hosted Sentry installation for this, because I wanted to be in control of the data. And guess what? The exceptions looked broken, just like the one above.
This article will explain what error reporting in a plugin can do for you. You’ll also learn how to implement it for your own plugin and how to avoid my mistake.
Why you should implement error reporting
The notorious blinking icon of IntelliJ
When you click on this red icon, then IntelliJ displays a dialog to report the exception.

How IntelliJ reports exceptions
Exceptions, which come from JetBrains’s code, are reported to their own servers. If the exception comes from the code of a 3rd–party plugin, then the error report is handled by that plugin. This is only possible if you implement the extension point for this.
If you don’t do that, then users still open the dialog, but can’t send the report because the “Submit” button is disabled. That’s confusing and frustrating.
If you care about your users and the quality of your plugin, then it’s a good idea to implement this. And it’s an easy, automated way to receive error reports with all the data you need.
Extension point “errorHandler”
The extension point we need is called com.intellij.errorHandler
.
Prerequisites
You need a few things before you can get to the code.
- A way to receive the reports
- You have multiple options here:
- An easy solution is to upload a cgi–script somewhere and to send a HTTP POST request. That’s what I’m doing with the open–source BashSupport plugin. It’s simple and easy to handle for small plugins or single developers.
- The client, whom I mentioned in the introduction, is using Rollbar. Rollbar is okay, but rather expensive if you need more than the basics.
- For BashSupport Pro I’m hosting Sentry on my own server. With Sentry you can either use their cloud-hosting solution or host the same software yourself. This way you get the complete set of features and still are in control of the data. You’ll need a hosting solution which supports Docker, though.
- Or you could do something completely different. For example, you could just open github.com in the browser or show a message to explain what the user should do.
- A privacy statement (optional)
- Users might want to know what’s send. You can provide your own message for the error reporting dialog.
- User identification (optional)
- The dialog provides optional UI to let the user provide identification, e.g. an email address or a login.
Implementation
This extension isn’t that complicated to implement. At first, I’ll describe how this works. You can find sample code to integrate with Sentry at the end.
Declare your error handler like this in your plugin.xml
:
|
|
You have to create a subclass of com.intellij.openapi.diagnostic.ErrorReportSubmitter. Implement at least these two methods:
public String getReportActionText()
. Use it to customize the label of the dialog’s submit button.public boolean submit(...)
. This method does all the hard work. The parameters provide the errors to report, an optional note of the user and a callback to tell IntelliJ when the report was send.
How to implement ErrorReportSubmitter.submit(...)
Here’s the full signature of the method:
|
|
The method itself has a boolean
return type. This is to tell the IDE if the report can be send at all. If you can’t send the report, then return false
and you’re done.
Otherwise, return true
and send the report asynchronously — that’s important.
events
- This is the list of exceptions, which should be send.
additionalInfo
- This is an optional message by the user.
parentComponent
- This might be useful if you want to show UI, e.g. a message box. Ignore this if you’re not interacting with the user.
consumer
- The callback. Call
consumer.consume(…)
when the report has been send successfully or failed to send. The argument to this method specifies the type of result.
Implementing an asynchronous operation might seem difficult at first. But IntelliJ already provides a bunch of abstractions to handle the most common cases.
IntelliJ’s own, internal error reporter implements this with a Task.Backgroundable
.
We’ll do this in a very similar manner:
|
|
Of course, the logic to send the data to your server isn’t there yet. But we’ll get to that, soon.
Extracting the data to send
Let’s have another look at the dialog and the signature of the submit()
method.

|
|
Most of the elements of this dialog can be customized by your implementation. From top to bottom:
- User message
- Here, a user may provide some more details. This message is provided by the parameter
String additionalInfo
. - Attachments
- A list of files, which should be send alongside the report.
- The first one is always the exception itself. Sometimes, IntelliJ adds more items to this list, e.g. the currently edited file. Only user–approved attachments are send. These attachments are made available by
com.intellij.diagnostic.IdeaReportingEvent
, which is a subclass ofIdeaLoggingEvent
. Logging events are provided by the parameterevents
. - User identification (optional, hidden by default)
- This allows to identify the user. Override
String getReporterAccount()
andvoid changeReporterAccount(Component parentComponent)
if you want to support this. This was only recently added to the SDK, versions 2019.3 and later come with this feature. - Privacy policy (optional, hidden by default)
- Implement
public String getPrivacyNoticeText()
if you’d like to show this. Basic HTML tags are allowed Versions 2018.3 and later offer this feature. - Submit button
- Implement method
String getReportActionText()
to customize the label of this button.
IdeaLoggingEvent
Do you still remember the first parameter of the submit()
method? It’s IdeaLoggingEvent[] events
. We’ll now take a closer look at these events.
As far as I can tell, IntelliJ always passes a single event of type IdeaReportingEvent
. IdeaReportingEvent
wraps the exception and the list of attachments. But this implementation may change at any time. So we’ll handle more than one event and won’t assume that it’s always a IdeaReportingEvent
.
Use event.getThrowableText()
to get the complete stack trace as a string.
Method event.getThrowable().printStackTrace(…)
provides the same value as event.getThrowableText()
.
But, please, never use the return value of event.getThrowable()
for anything else. For example, don’t use the result of event.getThrowable().getStackTrace()
.
Here’s why: the event is a IdeaReportingEvent
, and the implementation of IdeaReportingEvent is a bit special. It creates a new exception of type TextBasedThrowable
to wrap the original exception’s stack trace string. But its getStackTrace()
is still returning the stack trace where this wrapper was instantiated. If you use it, then you’ll get the wrong stack trace.
If you need the original exception, then retrieve it like this:
|
|
This is what happened for the plugin of my client:
- I implemented the error reporter in 2017. The exception returned by
event.getThrowable()
was passed on to the Rollbar library. Everything worked nicely. The Rollbar library usedevent.getThrowable().getStackTrace()
to get all the frames of the stack. So far, so good. - Now — in 2018 — this commit refactored the error reporting.
TextBasedThrowable
was introduced. Apparently, this change was made to allow user–editable stack traces. - Now,
event.getThrowable().getStackTrace()
now returns whereTextBasedThrowable
was created and not were the original exception occurred. - The error reporting is now messed up. Error reporting of my own plugins worked, because I was just using
event.getThrowableText()
and not the throwable itself.
Recommendations
- Use
event.getThrowableText()
whenever possible. Try to use the user–editable stack trace text. - Use
((AbstractMessage)event.getData()).getThrowable()
if you need the original exception with stack trace intact. This is probably the only solution for libraries, which need aThrowable
. And – of course – checkgetData()
fornull
when you’re using its return value.
As far as I know, this isn’t documented in the public API and this certainly is not an official guideline. Things may break in the future if you access the original throwable as shown above. But — to my knowledge — there’s no other way if you need it.
How to implement error reporting with Sentry
Here we’ll discuss in short how an implementation with Sentry could look like. There’s complete sample code on github.com for your reference. Here, we’re only discussing the general approach.
- Declare a dependency on the Sentry client library to make it available in your project. Add this to your build.gradle file:
|
|
- Setup the sentry client. This is usually only needed once. You need a “DSN” for this. Sentry provides it in the settings of your project. SentryDemo is a simple implementation.
- Create a new event with the properties, which you want to send together with the stack trace (sample code).
- Send the event to the Sentry server:
sentryClient.sendEvent(…)
. - Call
consumer.consume(…)
to tell the IDE about the result. You need to do this in the dispatcher thread. It’s usually something like this:
|
|
How this is displayed by Sentry
Here’s how such an exceptions shows up in Sentry. The tags ide.build
and release
are especially useful to debug these issues later. The automatic grouping of events is also very helpful.

Code of the Sentry error handler
This is just a copy of the code on GitHub.
|
|