{"slug": "haunted-stack-traces-explaining-java-errors-with-gemma-4-running-locally", "title": "Haunted Stack Traces: Explaining Java Errors with Gemma 4, Running Locally", "summary": "A developer built Haunted Stack Trace, a Spring Boot error handler that routes Java exception stack traces to Gemma 4 (12B) running locally through Ollama, returning a JSON diagnosis with the culprit line, cause, suggested fix and a themed \"ghost\" drawn from deprecated Java APIs such as java.util.Date and Vector. The project, an entry in the DEV Hacktoberfest 2026 Open-Source AI Challenge, runs entirely on-device with no API key or internet access. The developer reports that Ollama returned HTTP 501 for enforced JSON Schema structured output on this Gemma 4 build, so the handler instead relies on Spring AI's BeanOutputConverter to inject format instructions into the prompt and parse the reply.", "body_md": "*This is a submission for the [Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine](https://dev.to/challenges/hacktoberfest-week4-2026-10-26)*\n\nEvery Java developer knows the moment: the app crashes, the console fills with red, and somewhere in sixty lines of stack trace is the one line that matters.\n\n**The Haunted Stack Trace** is a Spring Boot error handler possessed by the ghosts of deprecated Java APIs. When the app throws an exception, it does not return a plain 500 page. The stack trace goes to **Gemma 4**, running locally through **Ollama**, and a ghost answers: `java.util.Date`, `Vector`, `Hashtable`, `Applet` or `StringTokenizer`, whichever fits the crime.\n\nThe ghost tells you:\n\n```\n{\n  \"ghost\": \"java.util.Date\",\n  \"exception\": \"java.time.format.DateTimeParseException\",\n  \"culprit\": \"...CursedController.halloweenDate(CursedController.java:63)\",\n  \"cause\": \"LocalDate.parse expects ISO-8601 (yyyy-MM-dd), but '31/10/2026' is in a different format.\",\n  \"fix\": \"Use LocalDate.parse(dateString, DateTimeFormatter.ofPattern(\\\"dd/MM/yyyy\\\")).\",\n  \"whisper\": \"You reach for the current moment, but you are trapped in a format of the past.\"\n}\n```\n\nI took the theme literally: the ghosts in the machine are the dead APIs we all still trip over. `java.util.Date` was replaced by `java.time`, `Vector` by `ArrayList`, and they are still haunting codebases. Here, at least, they are being useful.\n\nThere are two ways to wake them:\n\n`NullPointerException`, `ConcurrentModificationException`, `DateTimeParseException` and others)\n\nIt runs locally by design, so there is no deployed link. You can run it yourself in about five minutes (instructions in the repo), with your Wi-Fi off if you want to check my claim.\n\nA Spring Boot error handler possessed by the ghosts of deprecated Java APIs.\n\nWhen the application throws an exception, it does not return a plain 500 page. The stack trace is sent to **Gemma 4**, an open-weight model running locally through **Ollama**, and a ghost (`java.util.Date`, `Vector`, `Hashtable`, `Applet` or `StringTokenizer`) answers with what broke, the exact line, why it happened and how to fix it. Then it whispers something unsettling.\n\nEverything runs on your machine. No API key, no internet, and no stack trace ever leaves your laptop.\n\nBuilt as an entry for the DEV Hacktoberfest 2026 Open-Source AI Challenge (Week 4: Ghost in the Machine).\n\n`GET /cursed/date` calls `LocalDate.parse(\"31/10/2026\")`:\n\n```\n{\n  \"ghost\": \"java.util.Date\"\n  \"exception\": \"java.time.format.DateTimeParseException\",\n  \"culprit\": \"com.maurodc90.hauntedstacktrace.controller.CursedController.halloweenDate(CursedController.java:63)\",\n  \"cause\": \"The LocalDate.parse method expects\n```\n\n…\n**Stack:** Java 21, Spring Boot 4.1, Spring AI 2.0, Ollama, Gemma 4 (12B), plain HTML/CSS/JS, GitHub Actions.\n\nThe flow is short:\n\n`RuntimeException`.`@RestControllerAdvice` catches it and turns the stack trace into text.`GhostService` sends the `ChatClient`.` BeanOutputConverter` maps the reply onto a Java record, `Haunting`.\n\n```\nreturn chatClient.prompt()\n        .system(SYSTEM_PROMPT)\n        .user(trimmedTrace)\n        .options(OllamaChatOptions.builder()\n                .model(model)\n                .temperature(0.4)\n                .disableThinking()\n                .numPredict(400))\n        .call()\n        .entity(converter);\n```\n\nThat looks simple. Getting there was not, and the failures taught me more than the success did.\n\nMy first version asked Ollama to enforce a JSON Schema for the answer. Ollama replied with `HTTP 501 {\"error\":\"structured output is unavailable\"}` for this Gemma 4 build. Spring AI treated that as a transient error and retried with growing waits, 10 seconds, then 50, so my first request simply hung. I dropped the enforced schema and let `BeanOutputConverter` put the format instructions into the prompt and parse the reply. The structure still held.\n\nThen I typed something that was not a stack trace into the séance box: *\"give me a new error\"*. Six minutes later it was still summoning. `ollama ps` showed the model stuck on `Stopping...`. Nothing limited how long an answer could be.\n\nI added a cap of 400 tokens. Now real stack traces came back **empty**: `No content to map due to end-of-input`.\n\nThe cause: Gemma 4 *thinks* silently before it answers, and that reasoning counts towards the token budget. On nonsense input it reasoned forever. With a cap, it spent the whole budget thinking and never wrote the JSON. One line, `disableThinking()`, fixed both problems and made answers faster.\n\nThe real fix for nonsense input was to never send it to the model at all. The séance now checks for at least one stack frame (`at com.example.Foo.bar(Foo.java:42)`) before calling Gemma. Anything else gets an instant refusal from the ghost of Applet: *\"You knock, but bring no bones. The dead answer only to stack traces.\"*\n\n`Exception`.` favicon.ico` raises `NoResourceFoundException`. If I caught every `textContent`, never `innerHTML`.\nI pasted a trace from a bug in my own Expense Tracker: two Lombok `@Data` entities, `AppUser` and `Expense`, whose generated `hashCode()` methods call each other until the stack overflows. The ghost of Vector found the loop between the two classes immediately. But its fix was generic, because the trace never mentions Lombok. The real fix, `@EqualsAndHashCode.Exclude` on the relationship, was mine. The ghost is a good colleague, not an oracle, and I think that is the honest way to use these tools.\n\n**Because stack traces are not harmless.** They contain package names, class names, file paths, sometimes values. Pasting your company's stack traces into a hosted AI service means your company's code leaving the building. With an open-weight model on my own machine, nothing leaves. That is not a feature I bolted on. It is the reason the project exists.\n\nWhat open made possible here:\n\n`ollama ps` showed me the model stuck on `Stopping...`, and I restarted it myself. When answers came back empty, I could turn thinking off with one flag. With a closed API, I would have been looking at a timeout and guessing.`application.yml`. If 12B is too weak for someone's traces, they can try `gemma4:26b`. Spring AI's To be fair to closed models: a large hosted model would probably write better fixes for unusual traces. But for this job, explaining everyday exceptions on a developer's own laptop, a 12B open model was good enough, and its privacy was worth more than the extra quality.\n\nI built this pair-programming with Claude. Claude wrote most of the code. I decided what to build, ran and tested every step, hit every bug above on my own machine, chose the fixes, and reviewed the code until I could explain each part. I'm a junior Java developer, and working this way, fast with AI but understanding every line, is how I want to work.\n\nThe moment it landed was not a test passing. It was explaining the project to my partner, who does not write code.\n\nI said: imagine an app that tells you every day which chair I sit in at class. One day I don't come. The app looks for me, finds nothing, and crashes. That is a `NullPointerException`. Java's message says *what* happened, in a language only programmers read. This app explains *why*, in plain words, through a ghost.\n\nShe understood it in one go. That's the point of the project: a crash is just a ghost of something you expected to be there.", "url": "https://wpnews.pro/news/haunted-stack-traces-explaining-java-errors-with-gemma-4-running-locally", "canonical_source": "https://dev.to/maurodc90/haunted-stack-traces-explaining-java-errors-with-gemma-4-running-locally-35g0", "published_at": "2026-10-30 22:12:31+00:00", "updated_at": "2026-10-30 22:19:40.124392+00:00", "lang": "en", "topics": ["large-language-models", "ai-tools", "developer-tools", "ai-agents"], "entities": ["Gemma 4", "Ollama", "Spring Boot", "Spring AI", "Java", "GitHub Actions", "DEV Hacktoberfest"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/haunted-stack-traces-explaining-java-errors-with-gemma-4-running-locally", "markdown": "https://wpnews.pro/news/haunted-stack-traces-explaining-java-errors-with-gemma-4-running-locally.md", "text": "https://wpnews.pro/news/haunted-stack-traces-explaining-java-errors-with-gemma-4-running-locally.txt", "jsonld": "https://wpnews.pro/news/haunted-stack-traces-explaining-java-errors-with-gemma-4-running-locally.jsonld"}}