…start
`start()` handles the binary's output inside an `execFile` callback. The
empty-output branch called back with 'No output received' but did not
return, so control fell through to `data['message']['message']` on
`data = {}`. That threw a TypeError, and because the throw happens inside
a callback invoked by node's internal exithandler, no try/catch around
`local.start(...)` could intercept it — it surfaced as an
uncaughtException in the host process.
Three paths reached the same unguarded deref:
- empty stdout and stderr (the reported one) — now returns after the
callback, so it fires exactly once
- the terminal branch of the `error` handler, which also fell through
- any non-connected payload with no `message` key
Also guards `JSON.parse`: non-JSON output threw a SyntaxError from the
same uncatchable position, and is now reported through the callback with
the raw output attached as `extra`.
`startSync` shared the unguarded deref and now uses the same helper. Its
empty-output branch already returned, so it was not exposed to the
fall-through.
Adds regression tests driving start() with stub binaries for each output
shape, asserting the callback fires exactly once and nothing escapes as
an uncaughtException. They need no credentials or network. Three of the
four fail on master with the TypeError from the ticket.
Fixes an uncatchable TypeError thrown out of Local.start() when the BrowserStackLocal binary exits with no output.
JIRA Story: https://browserstack.atlassian.net/browse/LOC-7325
The bug
start() handles the binary's output inside an execFile callback. The empty-output branch called back with No output received but did not return, so control fell through to the next statement, which dereferences data['message']['message'] on data = {}:
TypeError: Cannot read properties of undefined (reading 'message') at .../browserstack-local/lib/Local.js:127:50Two things make this worse than a normal error path:
The trigger is not exotic — any environment where the binary exits without emitting JSON reaches it: wrong or blocked binary path, killed process, permission failure, or a shimmed binary in CI.
The fix
Three paths reached the same unguarded deref; all three are now closed:
Also guarded JSON.parse: non-JSON output (a plain-text crash message, for instance) threw a SyntaxError from the same uncatchable position. It is now reported through the callback as Invalid output received: <reason>, with the raw output attached as the error's extra field.
startSync shared the unguarded deref and now uses the same helper. Its empty-output branch already returned, so it was never exposed to the fall-through.
Every changed path now invokes the callback exactly once and lets the caller handle the failure normally.
Tests
Added test/local_start_output_handling.js — drives start() with stub binaries for each output shape and asserts the callback fires exactly once and that nothing escapes as an uncaughtException. No credentials or network needed.
Verified the tests actually catch the defect by toggling the fix:
Full suite, excluding the LocalBinary > Download block that needs real credentials:
Same 3 failures before and after (should return is running properly ×2, should stop local) — all pre-existing and credential-gated, none related to this change. npm run pretest (eslint over lib/* index.js) is clean.
Note: the fix avoids optional chaining because the repo's eslint config sets env: es6 (ES2015).
Scope
Code fix only — no version bump or publish here. 1.5.13 is the latest published version and carries the defect, so this needs a release to reach consumers.