JavaScript handles runtime errors with try/catch/finally blocks and the throw statement. Proper error handling makes tests and applications fail clearly instead of crashing silently.
Code that might fail goes in try; if an error is thrown, control jumps to catch(err) with the Error object; finally runs regardless of success or failure (cleanup, closing resources). You throw errors with throw new Error('message') — always throw Error objects (not strings) so you get a stack trace and a consistent .message/.name. For asynchronous code, a rejected promise is caught by .catch() or by try/catch around await. Unhandled rejections and thrown errors bubble up; in Node they can crash the process, so catch at appropriate boundaries. Custom error classes (class ValidationError extends Error) let callers distinguish error types.
A test teardown uses finally to always close a DB connection or browser context even if an assertion throws mid-test: try { await runSteps(); } catch (e) { logFailure(e); throw e; } finally { await browser.close(); } — guaranteeing cleanup regardless of pass/fail.
Does a return statement inside try skip the finally block? Explain the execution order.