> ## Content Index
> Fetch the complete content index at: https://www.ggorantala.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# APPLICATION FAILED TO START, and When You Don't Get It
- URL: https://www.ggorantala.dev/spring-boot-application-failed-to-start-explained/
- Published: 2026-10-02T05:18:52.000Z
- Updated: 2026-10-02T05:18:52.000Z
- Description: Spring Boot's failure banner only shows up when one of ~40 built-in analyzers recognizes your exception. Here's the catalog, and what prints when none do.
- Author: Gopi Gorantala
- Tags: spring-boot, spring-boot-errors, application-failed-to-start, failureanalyzer, spring-boot-4, startup-failures, spring-boot-diagnostics

There's no single Spring Boot failure banner. There are about forty of them, spread across roughly fourteen jars. If none of those forty recognizes what went wrong, you don't get a banner at all. You get a raw stack trace, `Application run failed`, and whatever `Caused by:` chain your JVM decided to print. Nobody tells you which situation you're in. You just learn to recognize the shape over time.

I'd used this banner for years before I actually went and found where it lives. It's a decade-old mechanism that's grown a lot more than it's changed, and Boot 4.1 quietly added a second, much smaller way to plug into it that most of the internet hasn't caught up to yet. This is the writeup: where the banner comes from, why it sometimes doesn't show up at all, and how you tell which case you're in without guessing.

## The banner you may or may not have gotten

Here's one, from a real (if artificial) failure: two configuration properties that shouldn't both be set at once.

```log

***************************
APPLICATION FAILED TO START
***************************

Description:

The following configuration properties are mutually exclusive:

	app.export.directory
	app.export.file

However, more than one of those properties has been configured at the same time:

	app.export.directory (originating from 'class path resource [application.properties] - 2:22')
	app.export.file (originating from 'class path resource [application.properties] - 1:17')

Action:

Update your configuration so that only one of the mutually exclusive properties is configured.
```

And here's the same class of mistake, a different exception, same Boot version, same app, same JVM:

```log
2026-09-16T09:14:02.311Z ERROR 41213 --- [           main] o.s.boot.SpringApplication              : Application run failed

java.lang.IllegalStateException: pretend third-party SDK could not find its license file
	at com.example.repro.demo.UnanalyzedFailureDemo.explode(UnanalyzedFailureDemo.java:13)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	...
```

No asterisks. No `Description:`. No `Action:`. The exception, logged once, at `ERROR`, and that's the whole message. Both of these come from a `@PostConstruct` method throwing during context refresh. Both kill the app the same way, same exit code, same dead process. One got the nice treatment. One didn't. That gap is the whole article.

Boot version for everything here is 4.1.1, Spring Framework 7.0.9, Java 21, Maven. This is a pure startup failure, both times: it happens during context refresh, before the app has taken a single request, and neither case needs a web server or a database to show up. I checked the mechanism itself back to 3.5.16 too, and it hasn't moved. What changed between those two releases is the size and shape of the catalog, which is section 8's problem.

## Is there even a catalog entry for what you're looking at

Before you go digging into why your `DataSource` won't build or your bean won't wire, spend ten seconds on a cheaper question: did Spring even recognize this failure as one it has a canned explanation for?

The tell is right there in the log. If you see the asterisk banner, something matched, and the `Description` and `Action` blocks are worth reading closely: they were built for this exact exception. If you see `Application run failed` followed by a bare stack trace, nothing matched, and you're on your own. Read the `Caused by:` chain from the bottom up, because that's where the real failure usually sits, several exceptions removed from the one on top.

There's no endpoint for this question. Actuator isn't up yet; the app never got that far, so `/actuator/conditions` and friends are the right move for a bean that got skipped during auto-configuration, not for a startup crash. The condition report from `--debug` won't help either. It tells you what got evaluated, not what got analyzed after the fact. The log line itself is the only place to look.

What you're really checking is whether your root cause belongs to the roughly forty classes Boot ships that know how to explain themselves. A short slice of that catalog, so you don't have to memorize all forty:

```log
NoSuchBeanDefinitionException             core/spring-boot-autoconfigure
BeanCurrentlyInCreationException          core/spring-boot
NoUniqueBeanDefinitionException           core/spring-boot
Port already in use (embedded server)     module/spring-boot-web-server
DataSource won't configure                module/spring-boot-jdbc
Hikari driver misconfiguration            module/spring-boot-jdbc
Liquibase changelog missing               module/spring-boot-liquibase
Tomcat connector fails to start           module/spring-boot-tomcat
Mutually exclusive config properties      core/spring-boot
@ConfigurationProperties validation       core/spring-boot
```

I've already written two of these up in depth: [the DataSource won't configure itself](https://ggorantala.dev/spring-boot-failed-to-configure-a-datasource/) case, and the [scan-root NoSuchBeanDefinitionException](https://ggorantala.dev/spring-boot-required-a-bean-of-type-that-could-not-be-found/) case. Both go through the exact mechanism below. `BeanCurrentlyInCreationException` and the port-in-use case are older posts on this site and go through it too, even though I didn't frame them that way at the time.

Worth saying plainly: `LazyInitializationException`, `TransactionRequiredException`, the CSRF 403, and the Bean Validation 500, four other failures I've written up here, are **not** in this catalog and never will be. They're request-time failures. This mechanism runs exactly once, during `SpringApplication.run()`, before your app has taken a single request. If your failure happened on request seventeen, you won't find it here. That's not a gap in the catalog. It's a boundary in what the catalog is for.

## Reproduce both outcomes on your laptop

No database, no Docker, no web server. This is a pure context-refresh failure, so the whole demo is two POJOs and a properties file.

**What you need**: Boot 4.1.1, Java 21, Maven. `start.spring.io` was unreachable for me again this run, eighth run straight at this point, so here's the pinned Initializr call and the dependency you'd add by hand if you're offline too:

```bash
curl https://start.spring.io/starter.zip \
  -d type=maven-project \
  -d bootVersion=4.1.1 \
  -d javaVersion=21 \
  -d groupId=com.example -d artifactId=repro -d name=repro \
  -d packageName=com.example.repro \
  -o repro.zip && unzip repro.zip -d repro && cd repro
```

```xml
<!-- the only dependency this needs beyond the parent BOM -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>
```

**The code.** A `@ConfigurationProperties` class that checks itself on `@PostConstruct`, using a static helper Boot already ships:

```java
package com.example.repro.export;

import jakarta.annotation.PostConstruct;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.source.MutuallyExclusiveConfigurationPropertiesException;

@ConfigurationProperties(prefix = "app.export")
public class ExportProperties {

	private String file;
	private String directory;

	public String getFile() { return this.file; }
	public void setFile(String file) { this.file = file; }
	public String getDirectory() { return this.directory; }
	public void setDirectory(String directory) { this.directory = directory; }

	@PostConstruct
	void checkMutuallyExclusive() {
		MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn((entries) -> {
			entries.put("app.export.file", this.file);
			entries.put("app.export.directory", this.directory);
		});
	}

}
```

```java
package com.example.repro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import com.example.repro.export.ExportProperties;

@SpringBootApplication
@EnableConfigurationProperties(ExportProperties.class)
public class ReproApplication {
	public static void main(String[] args) {
		SpringApplication.run(ReproApplication.class, args);
	}
}
```

A second bean covers the no-banner half, gated so it only fires when you ask for it:

```java
package com.example.repro.demo;

import jakarta.annotation.PostConstruct;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

@Component
@ConditionalOnProperty(name = "app.demo.trigger-unanalyzed-failure", havingValue = "true")
public class UnanalyzedFailureDemo {
	@PostConstruct
	void explode() {
		throw new IllegalStateException("pretend third-party SDK could not find its license file");
	}
}
```

**Trigger the banner.** In `application.properties`:

```properties
app.export.file=/var/exports/report.csv
app.export.directory=/var/exports
```

```log
Step 1 — build and run
$ ./mvnw -q spring-boot:run
Expected: dies in under 2 seconds. Last thing printed is the asterisk
banner from section 1, verbatim, including the mutually-exclusive-names
list sorted alphabetically. Exit code 1.
```

**Trigger the raw trace instead.** Comment out both `app.export.*` lines, add `app.demo.trigger-unanalyzed-failure=true`:

```log
Step 2 — same app, different property
$ ./mvnw -q spring-boot:run
Expected: dies just as fast, but now it's the bare stack trace from
section 1's second block. No asterisks anywhere in the output.
Exit code 1, same exit code as step 1, which is itself worth noticing.
```

**Confirmation signal.** Grep the output for `APPLICATION FAILED TO START`. Present in step 1, absent in step 2\. If you get neither, and instead a clean `Started ReproApplication`, you left both export properties unset or the demo property isn't `true`. If you see a different banner mentioning `IllegalStateException` wrapped as a `BeanCreationException`, you're on an older Boot where `@PostConstruct` failures get wrapped differently. That's a version difference worth noting, not a broken repro.

**Teardown.** There isn't any. No port got bound, no file got written, nothing's listening. `rm -rf repro` and you're done. That's a small point in this article's favor on its own: a pure diagnostics failure shouldn't need Docker to reproduce. If a "Spring Boot error" repro you find online insists on one, ask whether the failure is really about Spring Boot or about whatever's inside the container.

## The short version

The banner isn't generic. Every asterisk block you've seen was built by one specific Java class that recognized your specific exception and knew what to say about it. When startup fails, Boot hands the exception to a list of roughly forty of these classes, one at a time, and asks each one: do you know what this is? The first one to say yes wins, and its answer becomes the banner. If all forty say no, there's nothing left to try, and you get the exception printed the ordinary way, which is really just the JVM's default behavior with one logging line wrapped around it.

## Forty analyzers, fourteen jars, one loop

The interface is `FailureAnalyzer`, and it's been around since Boot 1.4.0\. This part of Spring Boot is genuinely old and hasn't changed shape since. One method, `analyze(Throwable)`, returns a `FailureAnalysis` (a description plus an optional action) or `null`. Most implementations extend `AbstractFailureAnalyzer<T>`, whose `analyze` does the boring part for you. It walks the entire cause chain looking for an instance of `T`, all the way down, past the outermost exception:

```java
public abstract class AbstractFailureAnalyzer<T extends Throwable> implements FailureAnalyzer {
	public FailureAnalysis analyze(Throwable failure) {
		T cause = findCause(failure, getCauseType());
		return (cause != null) ? analyze(failure, cause) : null;
	}
	// findCause walks failure.getCause() in a loop until it finds
	// an instanceof match, or runs out of chain.
}
```

That's why an analyzer written for `BeanCurrentlyInCreationException` still fires when the exception on top is some `UnsatisfiedDependencyException` wrapping it three levels down. It isn't matching the exception you see. It's matching something buried inside it.

Who runs the loop is a package-private class called, unglamorously, `FailureAnalyzers`. It loads every registered `FailureAnalyzer` off the classpath, appends one more at the very end (more on that shortly), and iterates:

```java
private FailureAnalysis analyze(Throwable failure, List<FailureAnalyzer> analyzers) {
	for (FailureAnalyzer analyzer : analyzers) {
		try {
			FailureAnalysis analysis = analyzer.analyze(failure);
			if (analysis != null) {
				return analysis;
			}
		}
		catch (Throwable ex) {
			// logged at TRACE, then the loop moves on
		}
	}
	return null;
}
```

First non-null answer wins. No scoring, no "best match," no tie-breaking by specificity. Whichever analyzer sits earlier in the list gets to answer, even if a later one would give a more precise explanation. And here's where it gets a little uncomfortable: that list order isn't documented or guaranteed anywhere. It's built by `SpringFactoriesLoader`, which calls `classLoader.getResources("META-INF/spring.factories")`, plain `ClassLoader.getResources()`, enumerating every `spring.factories` file across every jar on your classpath, and appends each file's entries in whatever order the classloader hands the jars back, then de-duplicates while keeping first-seen order. In practice that's your build tool's classpath order. It's stable for a given build, but not something you control cleanly, and definitely not something a `@Conditional` or an `@Order` can touch, because none of that machinery is running yet. Register your own `FailureAnalyzer` for an exception type Boot already has one for, and whoever wins is a function of jar ordering you didn't choose.

I went looking for whether this registration ever moved to the newer `AutoConfiguration.imports` file format, the way real auto-configuration classes did back in 2.7\. It didn't. Every module I checked, `spring-boot-jdbc`, `spring-boot-web-server`, `spring-boot-tomcat`, `spring-boot-liquibase`, all the way down, still registers its analyzers the old way: `META-INF/spring.factories`, key `org.springframework.boot.diagnostics.FailureAnalyzer`. Auto-configuration classes moved. This didn't. I don't have a clean reason why beyond "it works and nothing forced a change," and I'd rather say that plainly than invent a design rationale Spring's own team never wrote down anywhere I could find.

Now, the one appended at the end of every list, unconditionally: `FailureAnalyzedException::analyze`. This is new, `@since 4.1.0`, which means it landed in the release from this past August, and I haven't seen it mentioned outside Boot's own Javadoc yet. It's a plain public class you can extend directly:

```java
public class FailureAnalyzedException extends RuntimeException {
	public FailureAnalyzedException(String description, String action) { /* ... */ }
	public FailureAnalyzedException(String description, String action, Throwable cause) { /* ... */ }
}
```

Throw a subclass of it anywhere in your app and it gets the banner treatment automatically. No `FailureAnalyzer` class to write, no `spring.factories` entry to add. `FailureAnalyzedException::analyze` walks the chain the same way `AbstractFailureAnalyzer` does, checking `instanceof FailureAnalyzedException`, and builds the `FailureAnalysis` straight from whatever description and action you passed the constructor. It's checked dead last, after all forty registered analyzers, which makes sense: if Boot already has a real analyzer for your exception, that one should win over your generic self-report.

On the fallback side, when the loop returns `null`, or when it returns an analysis but every registered `FailureAnalysisReporter` is somehow empty, `SpringApplication.reportFailure` does exactly what step 2 of the repro showed:

```java
private void reportFailure(Collection<SpringBootExceptionReporter> exceptionReporters, Throwable failure) {
	for (SpringBootExceptionReporter reporter : exceptionReporters) {
		if (reporter.reportException(failure)) {
			registerLoggedException(failure);
			return;
		}
	}
	logger.error("Application run failed", failure);
	registerLoggedException(failure);
}
```

`registerLoggedException` is the other half of something I didn't expect to find. Boot installs its own `UncaughtExceptionHandler` on the main thread, `SpringBootExceptionHandler`, specifically so the JVM's default handler doesn't also print the same stack trace a second time once the exception propagates past `main()`. That same class is what turns a non-zero exit code into an actual `System.exit()` call. So the one, singular stack trace you see, and the process actually dying instead of just the thread quietly finishing, are both this small class's job. I'd never have gone looking for it if I hadn't been chasing this exact question anyway: why does one exception get the nice banner and the other doesn't. What felt like it should've been a five-minute answer took most of a day, because every file led to one more.

## What to change, and what you get for free

There's no "fix" here in the usual sense. The repro isn't broken; it's demonstrating correct behavior. But there's a real choice buried in it: how do you want your own startup checks to fail?

The version most people reach for, copied from a Baeldung post that's been circulating since roughly Boot 1.x, is a full custom `FailureAnalyzer`: a class implementing the interface, a registration entry, and your own exception type as the target. It still works, and it's still the right call when the exception you're analyzing comes from a library you don't own. You can't add a constructor to someone else's `RuntimeException`.

For your own exceptions, on 4.1+, none of that is necessary. Extend `FailureAnalyzedException`, throw it, done:

```java
public class ExportDestinationMisconfigured extends FailureAnalyzedException {
	public ExportDestinationMisconfigured(String file, String directory) {
		super(
			"Both app.export.file (" + file + ") and app.export.directory (" + directory
				+ ") are set. Export supports exactly one destination.",
			"Remove one of app.export.file or app.export.directory from your configuration.");
	}
}
```

What each approach costs: a full `FailureAnalyzer` is more code and one more registration file to remember exists, but it can inspect the original exception type, useful when you're analyzing something you can't modify. `FailureAnalyzedException` is two constructor arguments and nothing else to register, but it only works for exceptions you throw yourself, and it always loses a tie-break to a real analyzer for the same root cause, since it's checked last.

The non-fix worth flagging in review: hand-rolling your own mutual-exclusivity check as `if (a != null && b != null) throw new IllegalStateException(...)`. It isn't wrong exactly; it'll stop the app the same way. But you lose the free banner, the alphabetized property list, and the origin tracking, for no reason beyond not knowing `MutuallyExclusiveConfigurationPropertiesException.throwIfMultipleNonNullValuesIn()` already existed, since 2.6.0.

## Wire into the catalog instead of rolling your own

The better habit, once you know the catalog is there, is to reach for it before reaching for a plain exception. Boot ships more of these ready-made throw-and-forget helpers than the mutually-exclusive one: `BindValidationException` for `@ConfigurationProperties` validation failures, `InvalidConfigurationPropertyValueException` for "this property has a value, but it isn't a legal one," `UnboundConfigurationPropertyException` for the reverse case. Each already has an analyzer sitting in `core/spring-boot` waiting for it. If your own startup validation is throwing a bare `IllegalStateException` today, there's a real chance one of these already fits and you just haven't gone looking.

Where none of them fit, genuine custom validation, your own domain rule, that's exactly what 4.1's `FailureAnalyzedException` is for. I'd treat it as the default now, ahead of writing a full `FailureAnalyzer`, for anything where you own both the exception and the throw site.

## What's stable, what's new in 4.1, what to watch

The mechanism itself, `FailureAnalyzer`, `AbstractFailureAnalyzer`, `LoggingFailureAnalysisReporter`, the first-match-wins loop, is the oldest, steadiest part of this whole story. `@since 1.4.0`. It won't surprise you on an upgrade.

What will surprise you, jumping from a 3.x line straight to 4.1, is where things moved. In 3.5.16 the entire catalog lives in three files: `spring-boot`'s own `spring.factories` (twenty-ish analyzers, including the port-in-use one, right there in core), `spring-boot-autoconfigure`'s (the DataSource and Hikari ones, a Redis URL one, a handful more), and `spring-boot-actuator-autoconfigure`'s. Three files, thirty-eight analyzers, easy to `grep`. By 4.1.1 that same catalog, forty analyzers now, a couple net-new, is scattered across fourteen separate modules: `spring-boot-jdbc` has its own `spring.factories`, so does `spring-boot-web-server`, `spring-boot-tomcat`, `spring-boot-liquibase`, `spring-boot-data-redis`, `spring-boot-jooq`, `spring-boot-mail`, and more. The port-in-use analyzer specifically moved out of core into `module/spring-boot-web-server`. It isn't a removal. It's Boot 4's general modularization landing on this one corner too. But if you go looking for the catalog the old way, one `grep` across two familiar files, you'll only find a third of it.

If you're staying on 3.5.x for now (support runs to mid-2026 per Spring's own published policy, worth reconfirming against `spring.io/projects/spring-boot#support` whenever you're reading this, since I won't pretend a support table stays fixed), none of the `FailureAnalyzedException` material here applies to you yet. That constructor doesn't exist before 4.1.0\. Everything else, the loop, the classpath-order caveat, the catalog concept itself, has been true since long before 3.5 and will keep being true after 4.1.

## If you remember nothing else

The banner means an analyzer recognized your exception; a raw stack trace means none of roughly forty did, and that's a fact about the catalog, not a verdict on your code. The first matching analyzer wins, checked in an order Spring doesn't document and you don't control. Boot 4 spread that catalog across fourteen modules instead of three files, same size increase, much harder to `grep`. Since 4.1, if it's your own exception, you can skip writing a `FailureAnalyzer` entirely and just extend `FailureAnalyzedException`.

## Sources

- [FailureAnalyzer, AbstractFailureAnalyzer, FailureAnalyzers, LoggingFailureAnalysisReporter, FailureAnalyzedException](https://github.com/spring-projects/spring-boot/tree/v4.1.1/core/spring-boot/src/main/java/org/springframework/boot/diagnostics) (spring-boot v4.1.1)
- [SpringApplication.reportFailure / handleRunFailure](https://github.com/spring-projects/spring-boot/blob/v4.1.1/core/spring-boot/src/main/java/org/springframework/boot/SpringApplication.java) and [SpringBootExceptionHandler](https://github.com/spring-projects/spring-boot/blob/v4.1.1/core/spring-boot/src/main/java/org/springframework/boot/SpringBootExceptionHandler.java) (spring-boot v4.1.1)
- [MutuallyExclusiveConfigurationPropertiesException](https://github.com/spring-projects/spring-boot/blob/v4.1.1/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/MutuallyExclusiveConfigurationPropertiesException.java) (spring-boot v4.1.1)
- [SpringFactoriesLoader](https://github.com/spring-projects/spring-framework/blob/v7.0.9/spring-core/src/main/java/org/springframework/core/io/support/SpringFactoriesLoader.java) (spring-framework v7.0.9)
- [TextResourceOrigin / PropertySourceOrigin](https://github.com/spring-projects/spring-boot/blob/v4.1.1/core/spring-boot/src/main/java/org/springframework/boot/origin/) (spring-boot v4.1.1)
- Analyzer registration diffed directly via `spring.factories` in [spring-boot v3.5.16](https://github.com/spring-projects/spring-boot/blob/v3.5.16/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories) vs [v4.1.1](https://github.com/spring-projects/spring-boot/blob/v4.1.1/core/spring-boot/src/main/resources/META-INF/spring.factories), and across the fourteen `module/*` directories in the v4.1.1 tree
- [Spring Boot support policy](https://spring.io/projects/spring-boot#support), checked this run; Track A (Stack Overflow's Hot page and the Stack Exchange API) was unreachable through the egress proxy for the eighth consecutive run, so this pick came from evergreen search demand and the source itself rather than current SO activity