Error Tracking for Ember.js Apps: Run-Loop Errors, Route Failures, and Deprecations
Ember's run loop and container system catch errors differently than vanilla JavaScript. Here's how to track them, plus how to catch deprecations before an upgrade breaks production.
Picture a "Download Invoice" button that returns a blank PDF. No exceptions in the logs. No failed requests. Support tickets pile up before anyone connects the dots.
One way that happens in Ember: a deprecation warning, such as Ember.assign is deprecated, use Object.assign, sits in the dev console for months. It's easy to ignore because deprecations look like mere warnings. (They are not just warnings.) When the deprecated API is finally removed, a computed property throws and the PDF renderer gets an empty object instead of invoice data. The only stack trace sits in a browser console nobody is watching. Just a very quiet failure.
This is the problem with Ember.js error tracking: the framework is too good at handling errors gracefully. Once Ember.onerror is set, the run loop hands exceptions to it instead of letting them bubble to window.onerror, and unhandled RSVP rejections take the same route. Deprecation warnings that'll become breaking changes in the next major version only show up in dev and test builds, where nobody is reading them.
Wiring up proper error tracking for an Ember app takes an afternoon, not a weekend. Here's how.
What You'll Have by the End
A production-ready Ember.js error tracking setup that captures:
- Run-loop errors via
Ember.onerror - Promise rejections through
RSVP.on('error') - Route transition failures with full context
- Deprecation warnings from staging and test builds, tracked as low-severity events
- Pageviews tied to the same session for conversion correlation
All flowing into one dashboard. No more stitching together Sentry alerts and GA4 funnels at 2am.
Prerequisites
- Ember.js 4.x or 5.x
- Node.js 18+
- A JustAnalytics account on Pro or Enterprise: error tracking, alerts and conversion funnels are Pro features (the free tier covers analytics up to 100K events/month)
- Basic familiarity with Ember's application structure and initializers
If you're coming from another framework, we've covered Angular's Zone.js quirks, SvelteKit's handleError hook, and Vue/Nuxt setups separately. The concepts overlap, but the implementation differs.
Step 1: Add the Tracking Script
Add two JustAnalytics script tags to your app/index.html, before the closing </body> tag. tracker.js handles analytics events and pageviews; monitor.js handles error capture. Copy the exact snippet, including your site key, from your JustAnalytics dashboard:
<script src="https://justanalytics.app/tracker.js" async></script>
<script src="https://justanalytics.app/monitor.js" async></script>
The tracker already hooks pushState, replaceState and popstate, so Ember's router navigations register as pageviews without extra code. Step 3 adds route context to the errors that matter.
Add TypeScript declarations if you're using TS (and you probably should be in 2026):
// types/global.d.ts
declare global {
interface Window {
JustAnalytics?: {
track: (eventName: string, properties?: Record<string, unknown>) => void;
trackPageview: () => void;
};
JA?: {
captureException: (error: Error, options?: Record<string, unknown>) => void;
captureMessage: (message: string, level?: string) => void;
};
}
}
export {};
Step 2: Hook Into Ember.onerror and RSVP
This is where Ember error tracking diverges from every other framework. Once your app or an addon sets Ember.onerror, run-loop errors stop reaching window.onerror, so a generic SDK listening only there misses them.
Create an initializer at app/initializers/error-tracking.js:
// app/initializers/error-tracking.js
import Ember from 'ember';
import RSVP from 'rsvp';
export function initialize(application) {
// Catch synchronous errors in the run loop
Ember.onerror = function(error) {
trackError(error, { context: 'ember_onerror' });
// Re-throw in development so you see the stack trace
if (Ember.testing || application.resolveRegistration('config:environment').environment === 'development') {
throw error;
}
};
// Catch unhandled promise rejections from RSVP
RSVP.on('error', function(error) {
// This event fires only for rejections nobody handled
if (error && error.name === 'TransitionAborted') {
// Route transitions abort normally; not an error
return;
}
trackError(error, { context: 'rsvp_rejection' });
});
}
function trackError(error, metadata = {}) {
if (typeof window === 'undefined' || !window.JA) {
return;
}
const errorObj = error instanceof Error ? error : new Error(String(error));
window.JA.captureException(errorObj, {
...metadata,
url: window.location.href,
});
}
export default {
name: 'error-tracking',
initialize,
};
The TransitionAborted check is cheap insurance. A cancelled route transition, like a user clicking a link before the previous transition finishes, is normal behavior, not a bug. If one ever surfaces as an unhandled rejection, you don't want it flooding your dashboard with false positives from users clicking through quickly.
Step 3: Catch Route Transition Errors
Route transitions fail in ways that neither Ember.onerror nor RSVP catches—especially beforeModel hooks that redirect or abort.
Create a route mixin or base class. In Ember 5.x with Glimmer components, extend your application route:
// app/routes/application.js
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
export default class ApplicationRoute extends Route {
@service router;
constructor() {
super(...arguments);
// routeWillChange fires for every transition; record the ones that fail
this.router.on('routeWillChange', (transition) => {
transition.catch((error) => {
// Don't track intentional aborts
if (error.name === 'TransitionAborted') return;
this.trackRouteError(error, transition);
});
});
}
trackRouteError(error, transition) {
if (typeof window !== 'undefined' && window.JA) {
window.JA.captureException(error, {
context: 'route_transition',
from_route: transition.from?.name ?? 'initial',
to_route: transition.to?.name ?? 'unknown',
intent_url: transition.intent?.url,
});
}
}
}
This catches the errors that slip through the cracks: a model() hook that throws because an API returned 404, a beforeModel() that rejects when permissions check fails, a redirect() that points to a nonexistent route.
Step 4: Catch Deprecations Before Upgrading (Non-Production Only)
Here's the part that prevents the blank-PDF scenario from the intro—but with one essential constraint: Ember's production builds strip deprecate() and registerDeprecationHandler to no-ops, so a handler registered in production never fires. Deprecation tracking only works in non-production builds (development, test, staging). You must intentionally check deprecations before upgrading, not after production breaks.
Add to your app/app.js:
// app/app.js
import Application from '@ember/application';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from 'my-app/config/environment';
import { registerDeprecationHandler } from '@ember/debug';
// registerDeprecationHandler runs only in non-production builds.
// Set config.trackDeprecations = true in staging & CI test environments.
if (config.environment !== 'production' && config.trackDeprecations) {
registerDeprecationHandler((message, options, next) => {
// Always call next() to maintain the deprecation chain
next(message, options);
if (typeof window !== 'undefined' && window.JustAnalytics) {
window.JustAnalytics.track('deprecation_warning', {
deprecation_id: options.id,
deprecation_message: message.slice(0, 500),
deprecation_until: options.until,
url: window.location.href,
severity: 'low',
});
}
});
}
export default class App extends Application {
modulePrefix = config.modulePrefix;
podModulePrefix = config.podModulePrefix;
Resolver = Resolver;
}
loadInitializers(App, config.modulePrefix);
Define trackDeprecations in config/environment.js and set it to true for your staging environment and CI test runs. Then actively monitor: run your acceptance suite, or click through key flows on staging, and review the deprecations that appear in JustAnalytics.
Deprecations include an until field that tells you which version will remove the feature. Track that field, then filter your dashboard for deprecations until your next planned upgrade. The goal is to find and fix breaking changes before you upgrade, not after production breaks.
This is the discipline: upgrading across a major version without checking what lives in the deprecation warnings is the classic way to get surprised. It requires you to run the test suite and look at the results. If you skip this step or ignore the warnings, even with this setup in place, you'll hit the blank-PDF scenario anyway.
Step 5: Catch Ember Data Errors
Ember Data failures are sneaky. A failed save() doesn't throw synchronously—it returns a rejected promise. Server-side validation failures (a 422 that the adapter turns into an InvalidError) reject the promise AND simultaneously set record.isValid to false and fill record.errors. That second part is key: even though the promise rejects, the model state is updated, which is what makes these failures easy to miss.
For API errors during save:
// In your component or controller
async saveRecord() {
try {
await this.model.save();
} catch (error) {
// Track the save failure
if (window.JA) {
window.JA.captureException(error, {
context: 'ember_data_save',
model_type: this.model.constructor.modelName,
adapter_error: error.errors?.[0]?.detail,
});
}
this.errorMessage = 'Failed to save. Please try again.';
}
}
For validation errors, always check isValid inside the catch block. The promise rejection means the save failed, but the model state still tells you why:
// Inside the catch block after save() rejects
async saveRecord() {
try {
await this.model.save();
} catch (error) {
// Check the model's validation state even though the promise rejected
if (!this.model.isValid) {
const validationErrors = this.model.errors.toArray();
if (window.JustAnalytics) {
window.JustAnalytics.track('validation_failure', {
model_type: this.model.constructor.modelName,
field_errors: validationErrors.map(e => e.attribute).join(','),
error_count: validationErrors.length,
});
}
this.errorMessage = 'Please fix the errors below.';
return; // Don't fall through to the generic error handler
}
// Non-validation API error
if (window.JA) {
window.JA.captureException(error, {
context: 'ember_data_save',
model_type: this.model.constructor.modelName,
});
}
this.errorMessage = 'Failed to save. Please try again.';
}
}
This catches form abandonment that looks like ordinary form state—users hitting submit, seeing inline errors, and bouncing without completing the flow. If you're running conversion funnels, this data is gold.
Common Ember.js Error Tracking Problems and Fixes
Ember.onerror behaving differently in tests.
Ember's test helpers can override Ember.onerror during test runs. Guard your production tracking with an if (!Ember.testing) check, or better, mock the tracking function in tests. Don't let test failures pollute your production error logs.
RSVP errors that look like noise.
RSVP.on('error') fires only for rejections nobody handled, so a rejection you catch at the call site never reaches it. If you still see expected failures, the usual cause is a promise chain with no catch(), such as an AdapterError from a fire-and-forget save(). Handle it where it happens, and keep the TransitionAborted filter as a backstop.
Deprecations not appearing in production.
They won't. Production builds strip Ember's debug code, so deprecate() does nothing and registerDeprecationHandler never receives a call. That's by design, and easy to miss in the docs. Collect deprecations from a staging build or your test suite instead, as in Step 4.
Route errors not capturing query params.
The transition.to.queryParams object is available during routeWillChange but might be incomplete during routeDidChange. Capture what you can from the transition object, and supplement with window.location.search for the full picture.
What This Won't Fix
Container injection failures that happen before your initializer runs. If Ember can't resolve a service dependency, the app might not boot far enough to register your error tracking. Check the browser console, or your FastBoot server logs if you render on the server, for boot-time crashes. JustAnalytics structured logs can cover the server side if you want one place to look.
Third-party addon errors that swallow their own exceptions. Some addons catch errors internally and emit events instead. You'll need to subscribe to those addon-specific events. Addon READMEs sometimes document these events, but coverage is inconsistent. Frustrating, but that's the addon ecosystem for you.
Memory leaks from observers and computed properties. Those don't throw errors—they just make your app slow and unstable over time. Use Ember Inspector's render performance tab and track Web Vitals through JustAnalytics APM.
Next Steps
You've got Ember.js error tracking that actually catches Ember-specific failures. A few extensions worth adding:
Wire in session replay for visual context when errors occur. The GDPR-compliant replay setup works with Ember—the DOM capture is framework-agnostic. Useful for debugging component rendering issues that stack traces don't explain.
Set up alerts so you know when error rates spike before users complain. If your checkout flow usually has a 0.1% error rate and it jumps to 2%, that's worth a page. The same alerting covers cron and uptime checks.
Track custom events at funnel boundaries. Add window.JustAnalytics.track('checkout_started') and similar calls at key conversion points. Then compare sessions that hit an error with those that didn't to find which exceptions correlate with drop-off. That correlation is why running a unified stack matters—separate tools can't easily join error and funnel data.
The Deprecation Discipline
With deprecation tracking on staging and active monitoring, the blank-PDF scenario becomes preventable: you'd see the deprecation warning (e.g., "Ember.assign is deprecated, use Object.assign") during your test suite or staging smoke test, scheduled for removal in 5.0. You'd fix it before upgrading, not after production breaks. But this only works if you run the test suite, look at the deprecations, and act on them. If you ignore them or skip running tests, you'll still hit the blank-PDF scenario even with this setup in place. The tool tracks deprecations. Your discipline catches them.
Frequently Asked Questions
Why don't standard JavaScript error tracking tools catch Ember.js run-loop errors?
Ember's run loop schedules most framework work, including RSVP promise callbacks, in its own queues. When Ember.onerror is set, an exception thrown there goes to that hook instead of window.onerror. Generic error tracking SDKs instrument window.onerror and unhandledrejection, so once an app or addon sets Ember.onerror, those errors never reach them. You need to hook into Ember.onerror and RSVP.on('error') to catch what actually breaks in Ember apps.
How do I track Ember deprecation warnings if production builds strip them?
You can't, not from a production build. Ember strips deprecate() and registerDeprecationHandler down to no-ops in production, so nothing fires there. Register the handler in development, test and staging builds instead (a staging build that keeps debug code works well), and send each deprecation ID, message and until version to JustAnalytics as a low-severity event. Run your test suite or a staging smoke test against it and you see the breaking changes before the upgrade that removes them.
Can I use this setup with Ember.js apps deployed to Fastboot SSR?
Yes, with adjustments. Fastboot runs your app in Node.js for server-side rendering, so browser globals like window don't exist. Guard your tracking calls with typeof FastBoot !== 'undefined' checks, and send server-side errors through the Node SDK (@justanalyticsapp/node) instead of the browser scripts. The Ember.onerror and RSVP handlers work in both contexts—just change how you send the data.
Does this approach work with Ember Data model validation errors?
Yes. When the server rejects a save with a validation error, Ember Data rejects the save() promise with an InvalidError and also sets isValid to false and fills the model's errors. Catch the rejection around save(), then check isValid and send a custom event so the failure shows up in your funnel data. Without that step, the failure looks like ordinary form state and users who abandon at that point never appear as errors.
Try JustAnalytics
All-in-one observability in one under-5KB script: cookieless analytics + error tracking + APM + session replay + uptime + structured logs. Replaces GA4 + Sentry + Datadog + Pingdom + LogRocket. Free tier (100K events/mo), Pro $49/month ($39 annual).
Author at JustAnalytics.