Repository: rollup/rollup
File: src/rollup/rollup.ts (TypeScript)

Code before:
	const graph = new Graph(inputOptions, watcher);

	// remove the cache object from the memory after graph creation (cache is not used anymore)
	const useCache = rawInputOptions.cache !== false;
	if (rawInputOptions.cache) {
		inputOptions.cache = undefined;
		rawInputOptions.cache = undefined;
	}

	timeStart('BUILD', 1);

	await catchUnfinishedHookActions(graph.pluginDriver, async () => {
		try {
			timeStart('initialize', 2);
			await graph.pluginDriver.hookParallel('buildStart', [inputOptions]);
			timeEnd('initialize', 2);
			await graph.build();
		} catch (error_: any) {
			const watchFiles = Object.keys(graph.watchFiles);
			if (watchFiles.length > 0) {
				error_.watchFiles = watchFiles;
			}
			try {
				await graph.pluginDriver.hookParallel('buildEnd', [error_]);
			} catch (buildEndError: any) {
				await graph.pluginDriver.hookParallel('closeBundle', [buildEndError]);
				throw buildEndError;
			}
			await graph.pluginDriver.hookParallel('closeBundle', [error_]);
			throw error_;
		}
		try {
			await graph.pluginDriver.hookParallel('buildEnd', []);
		} catch (buildEndError: any) {
			await graph.pluginDriver.hookParallel('closeBundle', [buildEndError]);
			throw buildEndError;
		}
	});

	timeEnd('BUILD', 1);

	const result: RollupBuild = {
		cache: useCache ? graph.getCache() : undefined,
		async close() {
			if (result.closed) return;

			result.closed = true;

			await graph.pluginDriver.hookParallel('closeBundle', []);
		},
		closed: false,

Review comment (bug):
I wonder if we should pass the `buildEndError`, the original error, or maybe something different here? Also, one issue someone had that I vaguely remember is that the buildEndError could hide the original error, which makes debugging harder. On the other hand, we should not omit it, as the logic in buildEnd is obviously faulty.
One nice solution could be to throw a "concatenated error", e.g.

"There was an error during the build: [message]. Additionally, handling the error in buildEnd caused another error: [other message]"

And set the cause of the error to the first error. What do you think?

Code after:

	const graph = new Graph(inputOptions, watcher);

	// remove the cache object from the memory after graph creation (cache is not used anymore)
	const useCache = rawInputOptions.cache !== false;
	if (rawInputOptions.cache) {
		inputOptions.cache = undefined;
		rawInputOptions.cache = undefined;
	}

	timeStart('BUILD', 1);

	await catchUnfinishedHookActions(graph.pluginDriver, async () => {
		try {
			timeStart('initialize', 2);
			await graph.pluginDriver.hookParallel('buildStart', [inputOptions]);
			timeEnd('initialize', 2);
			await graph.build();
		} catch (error_: any) {
			const watchFiles = Object.keys(graph.watchFiles);
			if (watchFiles.length > 0) {
				error_.watchFiles = watchFiles;
			}
			try {
				await graph.pluginDriver.hookParallel('buildEnd', [error_]);
			} catch (buildEndError: any) {
				// Create a compound error object to include both errors, based on the original error
				const compoundError = getRollupError({
					...error_,
					message: `There was an error during the build:\n  ${error_.message}\nAdditionally, handling the error in the 'buildEnd' hook caused the following error:\n  ${buildEndError.message}`
				});
				await graph.pluginDriver.hookParallel('closeBundle', [compoundError]);
				throw compoundError;
			}
			await graph.pluginDriver.hookParallel('closeBundle', [error_]);
			throw error_;
		}
		try {
			await graph.pluginDriver.hookParallel('buildEnd', []);
		} catch (buildEndError: any) {
			await graph.pluginDriver.hookParallel('closeBundle', [buildEndError]);
			throw buildEndError;
		}
	});

	timeEnd('BUILD', 1);

	const result: RollupBuild = {
		cache: useCache ? graph.getCache() : undefined,
		async close() {
			if (result.closed) return;